@wrongstack/webui-server 0.301.0 → 0.302.2

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.
@@ -4323,7 +4323,8 @@ function createConversationOperations(ctx) {
4323
4323
  userMessage: async (ws, msg) => {
4324
4324
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
4325
4325
  const payload = msg.payload ?? {};
4326
- const controller = ctx.runControl.begin(ws);
4326
+ const originSessionId = ctx.getSessionId();
4327
+ const controller = ctx.runControl.begin(ws, originSessionId);
4327
4328
  if (!controller) {
4328
4329
  ctx.send(ws, {
4329
4330
  type: "error",
@@ -4334,7 +4335,6 @@ function createConversationOperations(ctx) {
4334
4335
  });
4335
4336
  return;
4336
4337
  }
4337
- const originSessionId = ctx.getSessionId();
4338
4338
  try {
4339
4339
  const agent = ctx.getAgent();
4340
4340
  if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
@@ -4393,12 +4393,13 @@ function createConversationOperations(ctx) {
4393
4393
  });
4394
4394
  }
4395
4395
  } finally {
4396
- ctx.runControl.end(ws, controller);
4396
+ ctx.runControl.end(ws, originSessionId, controller);
4397
4397
  }
4398
4398
  },
4399
4399
  abort: (ws, msg) => {
4400
4400
  if (!ensureCurrentSession(ws, msg, "abort")) return;
4401
- ctx.runControl.abort(ws);
4401
+ const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
4402
+ ctx.runControl.abort(ws, sessionId);
4402
4403
  ctx.notifyAbort(ws, {
4403
4404
  type: "error",
4404
4405
  payload: sessionPayload2({ phase: "abort", message: "User aborted" })
@@ -5450,6 +5451,7 @@ function createConnectionLifecycle(options) {
5450
5451
  }
5451
5452
 
5452
5453
  // src/server/connections-health-route.ts
5454
+ import * as net from "node:net";
5453
5455
  import {
5454
5456
  ChronicleProjectServerClient,
5455
5457
  createChronicleProjectAccess as createChronicleProjectAccess2,
@@ -5459,13 +5461,22 @@ import {
5459
5461
  isMailboxProjectServerAvailable,
5460
5462
  MailboxProjectServerConnection
5461
5463
  } from "@wrongstack/core/coordination";
5464
+ import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
5462
5465
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
5463
5466
  import {
5464
5467
  closeKanbanServerConnections,
5465
5468
  getKanbanServerConnection,
5466
5469
  isKanbanServerAvailable
5467
5470
  } from "@wrongstack/kanban";
5468
- import * as net from "node:net";
5471
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5472
+ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5473
+ import {
5474
+ checkCodebaseIndexServerHealth,
5475
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
5476
+ getIndexState,
5477
+ resolveProjectIndexDaemonAvailability,
5478
+ shutdownCodebaseIndexServer
5479
+ } from "@wrongstack/tools";
5469
5480
 
5470
5481
  // src/server/privileged-actions.ts
5471
5482
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -5499,15 +5510,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
5499
5510
  }
5500
5511
 
5501
5512
  // src/server/connections-health-route.ts
5502
- import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5503
- import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5504
- import {
5505
- checkCodebaseIndexServerHealth,
5506
- ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
5507
- getIndexState,
5508
- resolveProjectIndexDaemonAvailability,
5509
- shutdownCodebaseIndexServer
5510
- } from "@wrongstack/tools";
5511
5513
  async function handleConnectionsHealthRoute(context, ws, message) {
5512
5514
  if (message.type !== "connections.health") return false;
5513
5515
  try {
@@ -5528,6 +5530,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
5528
5530
  async function collectConnectionsHealth(options) {
5529
5531
  const services = await Promise.all([
5530
5532
  Promise.resolve(webuiHealth(options.backend)),
5533
+ sessionCatalogHealth(options.projectRoot),
5531
5534
  chronicleHealth(options.projectRoot),
5532
5535
  codebaseIndexHealth(options.projectRoot, options.indexDir),
5533
5536
  sageHealth(options.projectRoot),
@@ -5545,6 +5548,50 @@ async function collectConnectionsHealth(options) {
5545
5548
  services
5546
5549
  };
5547
5550
  }
5551
+ async function sessionCatalogHealth(projectRoot) {
5552
+ const startedAt = Date.now();
5553
+ try {
5554
+ const paths = resolveWstackPaths2({ projectRoot });
5555
+ const client = new SessionCatalogProjectClient({
5556
+ projectDir: paths.projectDir,
5557
+ projectRoot
5558
+ });
5559
+ try {
5560
+ const health = await client.ping();
5561
+ return {
5562
+ id: "session-catalog",
5563
+ label: "Session Catalog",
5564
+ status: health.damagedRows > 0 ? "degraded" : "healthy",
5565
+ required: true,
5566
+ mode: "project-daemon",
5567
+ 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).`,
5568
+ ownerPid: health.pid,
5569
+ endpoint: health.endpoint,
5570
+ storage: health.databasePath,
5571
+ uptimeMs: health.uptimeMs,
5572
+ latencyMs: Date.now() - startedAt,
5573
+ clients: health.clients,
5574
+ activeRequests: health.activeRequests,
5575
+ queuedWork: health.reservations + health.maintenanceLeases,
5576
+ control: "none"
5577
+ };
5578
+ } finally {
5579
+ await client.close().catch(() => void 0);
5580
+ }
5581
+ } catch (error2) {
5582
+ return {
5583
+ id: "session-catalog",
5584
+ label: "Session Catalog",
5585
+ status: "error",
5586
+ required: true,
5587
+ mode: "project-daemon",
5588
+ detail: "Project-scoped session ownership and catalog are unavailable.",
5589
+ latencyMs: Date.now() - startedAt,
5590
+ lastError: error2 instanceof Error ? error2.message : String(error2),
5591
+ control: "none"
5592
+ };
5593
+ }
5594
+ }
5548
5595
  function webuiHealth(backend) {
5549
5596
  return {
5550
5597
  id: "webui",
@@ -6143,7 +6190,11 @@ async function restartSageServer(projectRoot) {
6143
6190
  });
6144
6191
  const verifyConn = new SageProjectServerConnection(projectRoot);
6145
6192
  try {
6146
- await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
6193
+ await verifyConn.call(
6194
+ "ping",
6195
+ {},
6196
+ { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
6197
+ );
6147
6198
  return {
6148
6199
  serviceId: "sage",
6149
6200
  action: "restart",
@@ -6377,28 +6428,28 @@ async function restartMailboxServer(projectRoot) {
6377
6428
  var RESTART_POLL_INTERVAL_MS = 250;
6378
6429
  var RESTART_DEADLINE_MS = 3e3;
6379
6430
  function isEndpointAlive(endpoint) {
6380
- return new Promise((resolve15) => {
6431
+ return new Promise((resolve16) => {
6381
6432
  const sock = net.createConnection(endpoint);
6382
6433
  const timer = setTimeout(() => {
6383
6434
  sock.destroy();
6384
- resolve15(false);
6435
+ resolve16(false);
6385
6436
  }, 500);
6386
6437
  timer.unref?.();
6387
6438
  sock.once("connect", () => {
6388
6439
  clearTimeout(timer);
6389
6440
  sock.destroy();
6390
- resolve15(true);
6441
+ resolve16(true);
6391
6442
  });
6392
6443
  sock.once("error", () => {
6393
6444
  clearTimeout(timer);
6394
6445
  sock.destroy();
6395
- resolve15(false);
6446
+ resolve16(false);
6396
6447
  });
6397
6448
  });
6398
6449
  }
6399
6450
  async function waitForShutdown(probe) {
6400
6451
  if (!probe) {
6401
- await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6452
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6402
6453
  return;
6403
6454
  }
6404
6455
  const deadline = Date.now() + RESTART_DEADLINE_MS;
@@ -6409,7 +6460,7 @@ async function waitForShutdown(probe) {
6409
6460
  } catch {
6410
6461
  return;
6411
6462
  }
6412
- await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6463
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6413
6464
  }
6414
6465
  }
6415
6466
  function failureService(id, label, required, mode, error2, latencyMs) {
@@ -6572,9 +6623,9 @@ async function handleGitInfo(ws, projectRoot) {
6572
6623
  const cwd = projectRoot || void 0;
6573
6624
  try {
6574
6625
  const { execFile: ef } = await import("node:child_process");
6575
- const git = (args) => new Promise((resolve15) => {
6626
+ const git = (args) => new Promise((resolve16) => {
6576
6627
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
6577
- resolve15(err ? "" : stdout.trim());
6628
+ resolve16(err ? "" : stdout.trim());
6578
6629
  });
6579
6630
  });
6580
6631
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -6600,12 +6651,12 @@ async function handleGitInfo(ws, projectRoot) {
6600
6651
  function makeGit(cwd) {
6601
6652
  return async (args) => {
6602
6653
  const { execFile: ef } = await import("node:child_process");
6603
- return new Promise((resolve15) => {
6654
+ return new Promise((resolve16) => {
6604
6655
  ef(
6605
6656
  "git",
6606
6657
  args,
6607
6658
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
6608
- (err, stdout) => resolve15(err ? "" : stdout)
6659
+ (err, stdout) => resolve16(err ? "" : stdout)
6609
6660
  );
6610
6661
  });
6611
6662
  };
@@ -6770,7 +6821,7 @@ import { execFile } from "node:child_process";
6770
6821
  var GIT_TIMEOUT_MS = 1e4;
6771
6822
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
6772
6823
  function gitStdout(cwd, args) {
6773
- return new Promise((resolve15) => {
6824
+ return new Promise((resolve16) => {
6774
6825
  execFile(
6775
6826
  "git",
6776
6827
  [...args],
@@ -6781,7 +6832,7 @@ function gitStdout(cwd, args) {
6781
6832
  timeout: GIT_TIMEOUT_MS,
6782
6833
  maxBuffer: GIT_MAX_OUTPUT_BYTES
6783
6834
  },
6784
- (error2, stdout) => resolve15(error2 ? null : stdout)
6835
+ (error2, stdout) => resolve16(error2 ? null : stdout)
6785
6836
  );
6786
6837
  });
6787
6838
  }
@@ -7072,14 +7123,14 @@ var GoalWebSocketHandler = class {
7072
7123
  const cwd = env?.cwd ?? this.projectRoot;
7073
7124
  try {
7074
7125
  const { execFile: execFile2 } = await import("node:child_process");
7075
- const result = await new Promise((resolve15) => {
7126
+ const result = await new Promise((resolve16) => {
7076
7127
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7077
7128
  execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7078
7129
  if (err && err.code === "ENOENT") {
7079
- resolve15("[verify] tsc not found \u2014 skipping");
7130
+ resolve16("[verify] tsc not found \u2014 skipping");
7080
7131
  return;
7081
7132
  }
7082
- resolve15(stdout + stderr);
7133
+ resolve16(stdout + stderr);
7083
7134
  });
7084
7135
  });
7085
7136
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -7817,7 +7868,7 @@ function pushEvent(event) {
7817
7868
  }
7818
7869
  }
7819
7870
  function parseBody(req) {
7820
- return new Promise((resolve15, reject) => {
7871
+ return new Promise((resolve16, reject) => {
7821
7872
  let body = "";
7822
7873
  let bodyBytes = 0;
7823
7874
  let tooLarge = false;
@@ -7837,7 +7888,7 @@ function parseBody(req) {
7837
7888
  return;
7838
7889
  }
7839
7890
  try {
7840
- resolve15(JSON.parse(body));
7891
+ resolve16(JSON.parse(body));
7841
7892
  } catch {
7842
7893
  reject(new Error("Invalid JSON"));
7843
7894
  }
@@ -7924,7 +7975,7 @@ import * as path10 from "node:path";
7924
7975
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
7925
7976
  var MAX_BODY_BYTES = 10 * 1024 * 1024;
7926
7977
  function readJsonBody(req) {
7927
- return new Promise((resolve15, reject) => {
7978
+ return new Promise((resolve16, reject) => {
7928
7979
  const chunks = [];
7929
7980
  let total = 0;
7930
7981
  req.on("data", (chunk) => {
@@ -7936,7 +7987,7 @@ function readJsonBody(req) {
7936
7987
  }
7937
7988
  chunks.push(chunk);
7938
7989
  });
7939
- req.on("end", () => resolve15(Buffer.concat(chunks).toString("utf8")));
7990
+ req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
7940
7991
  req.on("error", (err) => reject(err));
7941
7992
  });
7942
7993
  }
@@ -8154,8 +8205,8 @@ async function handleApiSessions(res, globalRoot) {
8154
8205
  return;
8155
8206
  }
8156
8207
  try {
8157
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8158
- const registry = new SessionRegistry(globalRoot);
8208
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8209
+ const registry = getSessionRegistry3(globalRoot);
8159
8210
  const sessions = await registry.list();
8160
8211
  const result = sessions.map((s) => ({
8161
8212
  sessionId: s.sessionId,
@@ -8192,8 +8243,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8192
8243
  return;
8193
8244
  }
8194
8245
  try {
8195
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8196
- const registry = new SessionRegistry(globalRoot);
8246
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8247
+ const registry = getSessionRegistry3(globalRoot);
8197
8248
  const entry = await registry.get(sessionId);
8198
8249
  if (!entry) {
8199
8250
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8201,20 +8252,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8201
8252
  return;
8202
8253
  }
8203
8254
  res.writeHead(200, { "Content-Type": "application/json" });
8204
- res.end(JSON.stringify({
8205
- sessionId: entry.sessionId,
8206
- projectName: entry.projectName,
8207
- status: entry.status,
8208
- agents: entry.agents.map((a) => ({
8209
- id: a.id,
8210
- name: a.name,
8211
- status: a.status,
8212
- currentTool: a.currentTool,
8213
- iterations: a.iterations,
8214
- toolCalls: a.toolCalls,
8215
- lastActivityAt: a.lastActivityAt
8216
- }))
8217
- }));
8255
+ res.end(
8256
+ JSON.stringify({
8257
+ sessionId: entry.sessionId,
8258
+ projectName: entry.projectName,
8259
+ status: entry.status,
8260
+ agents: entry.agents.map((a) => ({
8261
+ id: a.id,
8262
+ name: a.name,
8263
+ status: a.status,
8264
+ currentTool: a.currentTool,
8265
+ iterations: a.iterations,
8266
+ toolCalls: a.toolCalls,
8267
+ lastActivityAt: a.lastActivityAt
8268
+ }))
8269
+ })
8270
+ );
8218
8271
  } catch (err) {
8219
8272
  res.writeHead(500, { "Content-Type": "application/json" });
8220
8273
  res.end(JSON.stringify({ error: sanitizeApiError(err) }));
@@ -8332,9 +8385,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8332
8385
  return;
8333
8386
  }
8334
8387
  try {
8335
- const { SessionRegistry, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8388
+ const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8336
8389
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8337
- const registry = new SessionRegistry(globalRoot);
8390
+ const registry = getSessionRegistry3(globalRoot);
8338
8391
  const entry = await registry.get(sessionId);
8339
8392
  if (!entry) {
8340
8393
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8342,7 +8395,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8342
8395
  return;
8343
8396
  }
8344
8397
  const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
8345
- const store = new DefaultSessionStore3({ dir: paths.projectSessions });
8398
+ const store = new DefaultSessionStore3({
8399
+ dir: paths.projectSessions,
8400
+ projectRoot: entry.projectRoot
8401
+ });
8346
8402
  const reader = new DefaultSessionReader2({ store });
8347
8403
  const RING = Math.max(limit * 4, 2e3);
8348
8404
  const ring = [];
@@ -8382,7 +8438,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8382
8438
  }
8383
8439
  }
8384
8440
  function readJsonBody2(req) {
8385
- return new Promise((resolve15, reject) => {
8441
+ return new Promise((resolve16, reject) => {
8386
8442
  const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8387
8443
  if (contentType !== "application/json") {
8388
8444
  reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
@@ -8398,7 +8454,7 @@ function readJsonBody2(req) {
8398
8454
  });
8399
8455
  req.on("end", () => {
8400
8456
  try {
8401
- resolve15(data ? JSON.parse(data) : {});
8457
+ resolve16(data ? JSON.parse(data) : {});
8402
8458
  } catch (err) {
8403
8459
  reject(err instanceof Error ? err : new Error(String(err)));
8404
8460
  }
@@ -8434,10 +8490,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
8434
8490
  const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
8435
8491
  const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
8436
8492
  try {
8437
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8493
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8438
8494
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8439
8495
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8440
- const registry = new SessionRegistry(globalRoot);
8496
+ const registry = getSessionRegistry3(globalRoot);
8441
8497
  const entry = await registry.get(sessionId);
8442
8498
  if (!entry) {
8443
8499
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8462,10 +8518,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
8462
8518
  return;
8463
8519
  }
8464
8520
  try {
8465
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8521
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8466
8522
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8467
8523
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8468
- const registry = new SessionRegistry(globalRoot);
8524
+ const registry = getSessionRegistry3(globalRoot);
8469
8525
  const entry = await registry.get(sessionId);
8470
8526
  if (!entry) {
8471
8527
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8522,10 +8578,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
8522
8578
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
8523
8579
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8524
8580
  try {
8525
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8581
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8526
8582
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8527
8583
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8528
- const registry = new SessionRegistry(globalRoot);
8584
+ const registry = getSessionRegistry3(globalRoot);
8529
8585
  const entry = await registry.get(sessionId);
8530
8586
  if (!entry) {
8531
8587
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8571,10 +8627,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
8571
8627
  }
8572
8628
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8573
8629
  try {
8574
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8630
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8575
8631
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8576
8632
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8577
- const registry = new SessionRegistry(globalRoot);
8633
+ const registry = getSessionRegistry3(globalRoot);
8578
8634
  const all = await registry.list();
8579
8635
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
8580
8636
  const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
@@ -8674,14 +8730,14 @@ async function readJsonBody3(res, req) {
8674
8730
  });
8675
8731
  return null;
8676
8732
  }
8677
- return new Promise((resolve15) => {
8733
+ return new Promise((resolve16) => {
8678
8734
  let data = "";
8679
8735
  let failed = false;
8680
8736
  const fail2 = (message) => {
8681
8737
  if (failed) return;
8682
8738
  failed = true;
8683
8739
  sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
8684
- resolve15(null);
8740
+ resolve16(null);
8685
8741
  };
8686
8742
  req.on("data", (chunk) => {
8687
8743
  if (failed) return;
@@ -8694,7 +8750,7 @@ async function readJsonBody3(res, req) {
8694
8750
  req.on("end", () => {
8695
8751
  if (failed) return;
8696
8752
  try {
8697
- resolve15(data.trim().length === 0 ? {} : JSON.parse(data));
8753
+ resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
8698
8754
  } catch {
8699
8755
  fail2("Request body is not valid JSON");
8700
8756
  }
@@ -9533,7 +9589,7 @@ function strictDecodeParam(segment, res) {
9533
9589
  function createHttpServer(opts) {
9534
9590
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
9535
9591
  const distDir = path13.resolve(opts.distDir);
9536
- const requireAccessToken = true;
9592
+ const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
9537
9593
  const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
9538
9594
  const trustedHostnames = (() => {
9539
9595
  const names = [...opts.allowedHostnames ?? []];
@@ -10079,9 +10135,9 @@ function createHttpServer(opts) {
10079
10135
  }
10080
10136
 
10081
10137
  // src/server/instance-registry.ts
10138
+ import * as fs11 from "node:fs/promises";
10082
10139
  import * as os from "node:os";
10083
10140
  import * as path14 from "node:path";
10084
- import * as fs11 from "node:fs/promises";
10085
10141
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
10086
10142
  function defaultBaseDir() {
10087
10143
  return path14.join(os.homedir(), ".wrongstack");
@@ -10434,11 +10490,11 @@ function kanbanListMessage(boards) {
10434
10490
  function kanbanDeletedMessage(boardId) {
10435
10491
  return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
10436
10492
  }
10437
- async function publishKanbanBoard(broadcast2, board, listBoards4) {
10493
+ async function publishKanbanBoard(broadcast2, board, listBoards5) {
10438
10494
  broadcast2(kanbanBoardMessage(board));
10439
- if (!listBoards4) return;
10495
+ if (!listBoards5) return;
10440
10496
  try {
10441
- broadcast2(kanbanListMessage(await listBoards4()));
10497
+ broadcast2(kanbanListMessage(await listBoards5()));
10442
10498
  } catch {
10443
10499
  }
10444
10500
  }
@@ -11074,6 +11130,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11074
11130
  fail(ws, type, `Board not found: ${boardId}`);
11075
11131
  return true;
11076
11132
  }
11133
+ if (ctx.supervisor) {
11134
+ const snapshots = await ctx.supervisor.auditNow(boardId);
11135
+ const snapshot = snapshots[0];
11136
+ if (snapshot) {
11137
+ ok(ws, type, snapshot);
11138
+ return true;
11139
+ }
11140
+ }
11077
11141
  const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
11078
11142
  let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11079
11143
  const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
@@ -11833,6 +11897,15 @@ function createShutdown(res) {
11833
11897
  } catch {
11834
11898
  }
11835
11899
  }
11900
+ if (res.onPreShutdown) {
11901
+ try {
11902
+ await res.onPreShutdown();
11903
+ } catch (e) {
11904
+ log(
11905
+ `[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
11906
+ );
11907
+ }
11908
+ }
11836
11909
  for (const server of res.servers) server?.close();
11837
11910
  if (res.onShutdown) {
11838
11911
  try {
@@ -13503,16 +13576,16 @@ function createModelOperations(context) {
13503
13576
  import * as net2 from "node:net";
13504
13577
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
13505
13578
  function isPortFree(host, port) {
13506
- return new Promise((resolve15) => {
13579
+ return new Promise((resolve16) => {
13507
13580
  const srv = net2.createServer();
13508
- srv.once("error", () => resolve15(false));
13581
+ srv.once("error", () => resolve16(false));
13509
13582
  srv.once("listening", () => {
13510
- srv.close(() => resolve15(true));
13583
+ srv.close(() => resolve16(true));
13511
13584
  });
13512
13585
  try {
13513
13586
  srv.listen(port, host);
13514
13587
  } catch {
13515
- resolve15(false);
13588
+ resolve16(false);
13516
13589
  }
13517
13590
  });
13518
13591
  }
@@ -13747,6 +13820,321 @@ async function handleBrainAsk(ctx, ws, question) {
13747
13820
  }
13748
13821
  }
13749
13822
 
13823
+ // src/server/kanban-supervisor.ts
13824
+ import {
13825
+ finalizeTaskCompletion,
13826
+ getBoard as getBoard2,
13827
+ getKanbanQueueHealth as getKanbanQueueHealth2,
13828
+ listBoards as listBoards3,
13829
+ reconcileKanbanBoard as reconcileKanbanBoard2,
13830
+ recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
13831
+ resolveGateEnforcement
13832
+ } from "@wrongstack/kanban";
13833
+ function resolveProjectRoot(deps2) {
13834
+ const root = deps2.projectRoot;
13835
+ return typeof root === "function" ? root() : root;
13836
+ }
13837
+ var DEFAULT_INTERVAL_MS = 1e4;
13838
+ var MIN_INTERVAL_MS = 2e3;
13839
+ var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
13840
+ var DEFAULT_CONFIG = {
13841
+ enabled: true,
13842
+ mode: "deterministic",
13843
+ intervalMs: DEFAULT_INTERVAL_MS,
13844
+ recoveryMode: "auto"
13845
+ };
13846
+ function createKanbanSupervisor(deps2) {
13847
+ const snapshots = /* @__PURE__ */ new Map();
13848
+ const nextDue = /* @__PURE__ */ new Map();
13849
+ const agentLastRun = /* @__PURE__ */ new Map();
13850
+ const agentRunning = /* @__PURE__ */ new Set();
13851
+ let disposed = false;
13852
+ let nextTimer;
13853
+ const forgetBoard = (boardId) => {
13854
+ snapshots.delete(boardId);
13855
+ nextDue.delete(boardId);
13856
+ agentLastRun.delete(boardId);
13857
+ agentRunning.delete(boardId);
13858
+ };
13859
+ const pruneAbsentBoards = (presentBoardIds) => {
13860
+ for (const boardId of snapshots.keys()) {
13861
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13862
+ }
13863
+ for (const boardId of nextDue.keys()) {
13864
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13865
+ }
13866
+ for (const boardId of agentLastRun.keys()) {
13867
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13868
+ }
13869
+ for (const boardId of agentRunning) {
13870
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13871
+ }
13872
+ };
13873
+ const publish = (snapshot) => {
13874
+ snapshots.set(snapshot.boardId, snapshot);
13875
+ deps2.broadcast({
13876
+ type: "kanban.supervisor.status",
13877
+ payload: { success: true, data: snapshot }
13878
+ });
13879
+ };
13880
+ const auditBoard = async (board) => {
13881
+ const config = effectiveConfig(board);
13882
+ const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
13883
+ const intervalMs = Math.max(MIN_INTERVAL_MS, config.intervalMs ?? DEFAULT_INTERVAL_MS);
13884
+ const nextAuditAt = new Date(Date.now() + intervalMs).toISOString();
13885
+ nextDue.set(board.id, Date.now() + intervalMs);
13886
+ if (!config.enabled) {
13887
+ const snapshot2 = {
13888
+ boardId: board.id,
13889
+ status: "disabled",
13890
+ mode: config.mode,
13891
+ lastAuditAt: auditedAt,
13892
+ nextAuditAt,
13893
+ reconciledTaskIds: [],
13894
+ staleRecoveredTaskIds: [],
13895
+ anomalyCount: 0,
13896
+ summary: "Supervision is disabled for this board."
13897
+ };
13898
+ publish(snapshot2);
13899
+ return snapshot2;
13900
+ }
13901
+ const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
13902
+ const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
13903
+ let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
13904
+ const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
13905
+ mode: config.recoveryMode ?? "auto",
13906
+ reason: "Kanban supervisor found an expired worker lease."
13907
+ }) : null;
13908
+ if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
13909
+ const anomalyCount = countAnomalies(health);
13910
+ const snapshot = {
13911
+ boardId: board.id,
13912
+ status: anomalyCount > 0 ? "attention" : "healthy",
13913
+ mode: config.mode,
13914
+ lastAuditAt: auditedAt,
13915
+ nextAuditAt,
13916
+ reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
13917
+ staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
13918
+ anomalyCount,
13919
+ summary: healthSummary(health)
13920
+ };
13921
+ publish(snapshot);
13922
+ const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
13923
+ if (changedBoard) {
13924
+ await publishKanbanBoard(
13925
+ deps2.broadcast,
13926
+ changedBoard,
13927
+ () => listBoards3(resolveProjectRoot(deps2))
13928
+ );
13929
+ }
13930
+ if (config.mode === "agentic" && anomalyCount > 0) {
13931
+ await maybeRunAgent(board, config, health, snapshot);
13932
+ }
13933
+ return snapshots.get(board.id) ?? snapshot;
13934
+ };
13935
+ const maybeRunAgent = async (board, config, health, snapshot) => {
13936
+ if (!deps2.dispatchTask || agentRunning.has(board.id)) return;
13937
+ const cooldownMs = Math.max(
13938
+ MIN_INTERVAL_MS,
13939
+ config.agentCooldownMs ?? DEFAULT_AGENT_COOLDOWN_MS
13940
+ );
13941
+ if (Date.now() - (agentLastRun.get(board.id) ?? 0) < cooldownMs) return;
13942
+ agentRunning.add(board.id);
13943
+ agentLastRun.set(board.id, Date.now());
13944
+ const watchdog = setTimeout(
13945
+ () => agentRunning.delete(board.id),
13946
+ Math.max(cooldownMs * 2, DEFAULT_AGENT_COOLDOWN_MS * 2)
13947
+ );
13948
+ watchdog.unref?.();
13949
+ publish({
13950
+ ...snapshot,
13951
+ status: "running",
13952
+ lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
13953
+ summary: `Agentic anomaly review started. ${snapshot.summary ?? ""}`.trim()
13954
+ });
13955
+ const routing = config.routing ?? { mode: "session" };
13956
+ try {
13957
+ const spawnSummary = await deps2.dispatchTask(buildAuditPrompt(board, health), {
13958
+ ...dispatchRoute(routing),
13959
+ ...config.skills?.length ? { skills: config.skills } : {},
13960
+ name: `kanban-supervisor-${board.id.slice(0, 6)}`,
13961
+ // Carry the board identity into the spawned TaskSpec.context so the
13962
+ // tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
13963
+ // the live board policy instead of failing open. Whole-board agentic
13964
+ // runs have no taskId, so only boardId is propagated.
13965
+ context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
13966
+ onDone: async (result) => {
13967
+ clearTimeout(watchdog);
13968
+ agentRunning.delete(board.id);
13969
+ if (await getBoard2(resolveProjectRoot(deps2), board.id) === null) return;
13970
+ const current3 = snapshots.get(board.id) ?? snapshot;
13971
+ publish({
13972
+ ...current3,
13973
+ status: result.status === "failed" ? "error" : current3.anomalyCount ? "attention" : "healthy",
13974
+ lastAgentRunAt: (/* @__PURE__ */ new Date()).toISOString(),
13975
+ summary: result.result ?? current3.summary,
13976
+ ...result.error ? { error: result.error } : {}
13977
+ });
13978
+ }
13979
+ });
13980
+ const current2 = snapshots.get(board.id) ?? snapshot;
13981
+ publish({ ...current2, status: "running", summary: spawnSummary });
13982
+ } catch (error2) {
13983
+ clearTimeout(watchdog);
13984
+ agentRunning.delete(board.id);
13985
+ const message = error2 instanceof Error ? error2.message : String(error2);
13986
+ deps2.log?.(`[KanbanSupervisor] ${board.id}: ${message}`);
13987
+ publish({ ...snapshot, status: "error", error: message });
13988
+ }
13989
+ };
13990
+ const auditNow = async (boardId) => {
13991
+ let boards;
13992
+ if (boardId) {
13993
+ const board = await getBoard2(resolveProjectRoot(deps2), boardId);
13994
+ if (board === null) {
13995
+ forgetBoard(boardId);
13996
+ boards = [];
13997
+ } else {
13998
+ boards = [board];
13999
+ }
14000
+ } else {
14001
+ const summaries = await listBoards3(resolveProjectRoot(deps2));
14002
+ pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
14003
+ boards = (await Promise.all(summaries.map((summary) => getBoard2(resolveProjectRoot(deps2), summary.id)))).filter((board) => Boolean(board));
14004
+ }
14005
+ const results = [];
14006
+ for (const board of boards) results.push(await auditBoard(board));
14007
+ scheduleNext();
14008
+ return results;
14009
+ };
14010
+ const scheduleNext = () => {
14011
+ if (disposed) return;
14012
+ if (nextTimer !== void 0) {
14013
+ clearTimeout(nextTimer);
14014
+ nextTimer = void 0;
14015
+ }
14016
+ const now = Date.now();
14017
+ let minDue = Infinity;
14018
+ for (const due of nextDue.values()) {
14019
+ if (due < minDue) minDue = due;
14020
+ }
14021
+ if (!Number.isFinite(minDue) || minDue <= now) {
14022
+ nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
14023
+ nextTimer.unref?.();
14024
+ return;
14025
+ }
14026
+ const delay = Math.min(minDue - now, DEFAULT_INTERVAL_MS);
14027
+ if (delay <= 0) {
14028
+ nextTimer = setTimeout(() => void tick(), MIN_INTERVAL_MS);
14029
+ } else {
14030
+ nextTimer = setTimeout(() => void tick(), delay);
14031
+ }
14032
+ nextTimer.unref?.();
14033
+ };
14034
+ const tick = async () => {
14035
+ if (disposed) return;
14036
+ try {
14037
+ const now = Date.now();
14038
+ const summaries = await listBoards3(resolveProjectRoot(deps2));
14039
+ pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
14040
+ for (const summary of summaries) {
14041
+ if ((nextDue.get(summary.id) ?? 0) > now) continue;
14042
+ const board = await getBoard2(resolveProjectRoot(deps2), summary.id);
14043
+ if (board) await auditBoard(board);
14044
+ }
14045
+ } catch (error2) {
14046
+ deps2.log?.(`[KanbanSupervisor] ${error2 instanceof Error ? error2.message : String(error2)}`);
14047
+ } finally {
14048
+ scheduleNext();
14049
+ }
14050
+ };
14051
+ void scheduleNext();
14052
+ return {
14053
+ getSnapshot: (boardId) => snapshots.get(boardId),
14054
+ auditNow,
14055
+ getStats: () => ({
14056
+ snapshots: snapshots.size,
14057
+ scheduledBoards: nextDue.size,
14058
+ agentCooldowns: agentLastRun.size,
14059
+ runningAgents: agentRunning.size
14060
+ }),
14061
+ dispose() {
14062
+ disposed = true;
14063
+ if (nextTimer !== void 0) {
14064
+ clearTimeout(nextTimer);
14065
+ nextTimer = void 0;
14066
+ }
14067
+ snapshots.clear();
14068
+ nextDue.clear();
14069
+ agentLastRun.clear();
14070
+ agentRunning.clear();
14071
+ }
14072
+ };
14073
+ }
14074
+ function effectiveConfig(board) {
14075
+ return { ...DEFAULT_CONFIG, ...board.supervisor ?? {} };
14076
+ }
14077
+ async function sweepGateParkedTasks(deps2, board) {
14078
+ if (resolveGateEnforcement(board) === "off") return void 0;
14079
+ const parked = board.tasks.filter(
14080
+ (task) => task.status === "review" && task.assignment?.status === "completed" && !task.verificationReport
14081
+ );
14082
+ let lastBoard;
14083
+ for (const task of parked) {
14084
+ try {
14085
+ const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
14086
+ eventContext: { actor: "kanban-supervisor" }
14087
+ });
14088
+ if (finalized) lastBoard = finalized.board;
14089
+ } catch (error2) {
14090
+ deps2.log?.(
14091
+ `[KanbanSupervisor] completion gate sweep failed for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
14092
+ );
14093
+ }
14094
+ }
14095
+ return lastBoard;
14096
+ }
14097
+ function dispatchRoute(routing) {
14098
+ if (routing.mode === "session") return {};
14099
+ return {
14100
+ ...routing.provider ? { provider: routing.provider } : {},
14101
+ ...routing.model ? { model: routing.model } : {},
14102
+ ...routing.fallbackProfile ? { fallbackProfile: routing.fallbackProfile } : {},
14103
+ ...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
14104
+ };
14105
+ }
14106
+ function countAnomalies(health) {
14107
+ return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
14108
+ }
14109
+ function healthSummary(health) {
14110
+ return [
14111
+ `${health.counts.running} running`,
14112
+ `${health.counts.ready} ready`,
14113
+ `${health.counts.review} review`,
14114
+ `${health.counts.blocked} blocked`,
14115
+ `${health.counts.failed} failed`,
14116
+ `${health.staleAssignments.count} stale`,
14117
+ `${health.dependencyBlocked.count} dependency-blocked`
14118
+ ].join(" \xB7 ");
14119
+ }
14120
+ function buildAuditPrompt(board, health) {
14121
+ const taskLines = board.tasks.map(
14122
+ (task) => `- ${task.id}: ${task.title} [task=${task.status}; assignment=${task.assignment?.status ?? "none"}; column=${task.columnId}]`
14123
+ );
14124
+ return [
14125
+ "You are the explicitly configured WrongStack Kanban supervisor.",
14126
+ "Audit only this board. Do not implement product tasks.",
14127
+ `Board: ${board.title} (${board.id})`,
14128
+ `Health: ${healthSummary(health)}`,
14129
+ "",
14130
+ "Tasks:",
14131
+ ...taskLines,
14132
+ "",
14133
+ "Use the kanban tool for corrections. Preserve manual blockers and dependencies.",
14134
+ "Fix only demonstrable status/assignment/column drift, then report every action and remaining anomaly."
14135
+ ].join("\n");
14136
+ }
14137
+
13750
14138
  // src/server/context-meta.ts
13751
14139
  import { FallbackProfileManager } from "@wrongstack/core/agent";
13752
14140
  import { resolvePluginEnablement } from "@wrongstack/core/plugin";
@@ -16222,6 +16610,8 @@ function labelForEvent(e) {
16222
16610
  const count = e.messagesOmitted ?? e.messages.length;
16223
16611
  return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
16224
16612
  }
16613
+ case "messages_dropped":
16614
+ return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
16225
16615
  case "message_truncated":
16226
16616
  return `Message truncated: ${e.before} \u2192 ${e.after}`;
16227
16617
  case "file_event":
@@ -16309,6 +16699,8 @@ function detailForEvent(e) {
16309
16699
  return `at index ${e.index}`;
16310
16700
  case "messages_replaced":
16311
16701
  return `${e.messagesOmitted ?? e.messages.length} total`;
16702
+ case "messages_dropped":
16703
+ return `dropped ${e.count} from the front`;
16312
16704
  case "message_truncated":
16313
16705
  return `truncated to ${e.after} tokens`;
16314
16706
  case "mode_changed":
@@ -16526,7 +16918,7 @@ function createSessionHandlers(ctx) {
16526
16918
  const current2 = ctx.getSession();
16527
16919
  if (current2 !== next) {
16528
16920
  try {
16529
- ctx.abortActiveRun?.();
16921
+ ctx.abortActiveRun?.(current2.id);
16530
16922
  } catch {
16531
16923
  }
16532
16924
  await finalizeSession(current2);
@@ -18211,7 +18603,7 @@ async function handleShellOpen(req, logger, options) {
18211
18603
  import {
18212
18604
  enqueueKanbanWorkflowCommand,
18213
18605
  kanbanWorkflowId,
18214
- listBoards as listBoards3,
18606
+ listBoards as listBoards4,
18215
18607
  listKanbanWorkflowStates
18216
18608
  } from "@wrongstack/kanban";
18217
18609
  import {
@@ -18391,7 +18783,7 @@ var SddBoardWebSocketHandler = class {
18391
18783
  this.broadcast({ type: "sdd.board.snapshot", payload: null });
18392
18784
  if (this.lifecycle) {
18393
18785
  try {
18394
- const boards = await listBoards3(this.lifecycle.projectRoot);
18786
+ const boards = await listBoards4(this.lifecycle.projectRoot);
18395
18787
  this.broadcast({
18396
18788
  type: "kanban.list",
18397
18789
  payload: { success: true, data: boards }
@@ -19093,7 +19485,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
19093
19485
  }
19094
19486
 
19095
19487
  // src/server/setup-events-fleet-broadcaster.ts
19096
- import { watch as fsWatch } from "node:fs";
19097
19488
  import * as path20 from "node:path";
19098
19489
  function registerSetupEventsFleetBroadcaster(deps2) {
19099
19490
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
@@ -19102,8 +19493,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
19102
19493
  const disposers = [];
19103
19494
  const broadcastSessions = async () => {
19104
19495
  try {
19105
- const { SessionRegistry } = await import("@wrongstack/core/storage");
19106
- const registry = new SessionRegistry(globalRoot);
19496
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
19497
+ const registry = getSessionRegistry3(globalRoot);
19107
19498
  const sessions = await registry.list();
19108
19499
  const ownEntry = sessions.find((s) => s.pid === process.pid);
19109
19500
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
@@ -19150,7 +19541,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
19150
19541
  }
19151
19542
  };
19152
19543
  onFleetBroadcaster?.(broadcastSessions);
19153
- let regWatchLive = false;
19544
+ let subscriptionLive = false;
19154
19545
  let statusTimer;
19155
19546
  const scheduleStatusPoll = () => {
19156
19547
  if (isDisposed()) return;
@@ -19159,35 +19550,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
19159
19550
  void broadcastSessions();
19160
19551
  scheduleStatusPoll();
19161
19552
  },
19162
- regWatchLive ? 3e4 : 5e3
19553
+ subscriptionLive ? 3e4 : 5e3
19163
19554
  );
19164
19555
  if (statusTimer.unref) statusTimer.unref();
19165
19556
  };
19166
19557
  disposers.push(() => {
19167
19558
  if (statusTimer) clearTimeout(statusTimer);
19168
19559
  });
19169
- let regDebounce;
19170
- try {
19171
- const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
19172
- const name2 = filename ? String(filename) : "";
19173
- if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
19174
- if (regDebounce) clearTimeout(regDebounce);
19175
- regDebounce = setTimeout(() => void broadcastSessions(), 150);
19176
- });
19177
- regWatcher.on("error", () => {
19178
- regWatchLive = false;
19179
- try {
19180
- regWatcher.close();
19181
- } catch {
19182
- }
19183
- });
19184
- regWatchLive = true;
19185
- disposers.push(() => {
19186
- if (regDebounce) clearTimeout(regDebounce);
19187
- regWatcher.close();
19188
- });
19189
- } catch {
19190
- }
19560
+ let eventDebounce;
19561
+ let unsubscribe;
19562
+ void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
19563
+ const registry = getSessionRegistry3(globalRoot);
19564
+ const projectSlug2 = wpaths?.projectSlug;
19565
+ if (!projectSlug2 || isDisposed()) return;
19566
+ unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
19567
+ if (eventDebounce) clearTimeout(eventDebounce);
19568
+ eventDebounce = setTimeout(() => void broadcastSessions(), 25);
19569
+ });
19570
+ subscriptionLive = true;
19571
+ }).catch(() => {
19572
+ subscriptionLive = false;
19573
+ });
19574
+ disposers.push(() => {
19575
+ if (eventDebounce) clearTimeout(eventDebounce);
19576
+ void unsubscribe?.();
19577
+ });
19191
19578
  scheduleStatusPoll();
19192
19579
  void broadcastSessions();
19193
19580
  return () => {
@@ -19300,7 +19687,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
19300
19687
  }
19301
19688
 
19302
19689
  // src/server/setup-events-status-watcher.ts
19303
- import { watch as fsWatch2 } from "node:fs";
19690
+ import { watch as fsWatch } from "node:fs";
19304
19691
  import * as fs18 from "node:fs/promises";
19305
19692
  import * as path22 from "node:path";
19306
19693
 
@@ -19390,7 +19777,7 @@ function registerSetupEventsStatusWatcher(deps2) {
19390
19777
  try {
19391
19778
  await fs18.mkdir(projectsDir, { recursive: true });
19392
19779
  if (isDisposed()) return;
19393
- watcher = fsWatch2(
19780
+ watcher = fsWatch(
19394
19781
  projectsDir,
19395
19782
  { persistent: true, recursive: true },
19396
19783
  async (eventType, filename) => {
@@ -20552,7 +20939,6 @@ import {
20552
20939
  mailboxSessionTag,
20553
20940
  ObservableBrainArbiter as ObservableBrainArbiterCtor
20554
20941
  } from "@wrongstack/core/coordination";
20555
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
20556
20942
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20557
20943
  import {
20558
20944
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -20564,6 +20950,7 @@ import {
20564
20950
  import { TOKENS } from "@wrongstack/core/kernel";
20565
20951
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
20566
20952
  import {
20953
+ DEFAULT_TOOLS_CONFIG,
20567
20954
  resolveContextWindowPolicy as resolveContextWindowPolicy2
20568
20955
  } from "@wrongstack/core/types";
20569
20956
  import {
@@ -20724,7 +21111,7 @@ function findWorkspaceCliEntry(projectRoot) {
20724
21111
  return null;
20725
21112
  }
20726
21113
  function sleep(ms) {
20727
- return new Promise((resolve15) => setTimeout(resolve15, ms));
21114
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
20728
21115
  }
20729
21116
 
20730
21117
  // src/server/terminal-ws-handler.ts
@@ -20967,7 +21354,7 @@ function clampDim(value, fallback) {
20967
21354
  }
20968
21355
 
20969
21356
  // src/server/worktree-ws-handler.ts
20970
- import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
21357
+ import { join as join11, resolve as resolve13, sep as sep5 } from "node:path";
20971
21358
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
20972
21359
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20973
21360
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -21052,11 +21439,11 @@ var WorktreeWebSocketHandler = class {
21052
21439
  // ── orphan management ─────────────────────────────────────────────────────
21053
21440
  /** Absolute managed-worktrees root for this project. */
21054
21441
  worktreesRoot() {
21055
- return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
21442
+ return resolve13(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
21056
21443
  }
21057
21444
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
21058
21445
  underRoot(dir) {
21059
- const abs = resolve12(dir);
21446
+ const abs = resolve13(dir);
21060
21447
  const root = this.worktreesRoot();
21061
21448
  return abs !== root && abs.startsWith(root + sep5);
21062
21449
  }
@@ -21280,7 +21667,7 @@ var WorktreeWebSocketHandler = class {
21280
21667
  }
21281
21668
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
21282
21669
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
21283
- const summary = await wt.diffSummary(resolve12(dir), base);
21670
+ const summary = await wt.diffSummary(resolve13(dir), base);
21284
21671
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
21285
21672
  }
21286
21673
  // ── internals ───────────────────────────────────────────────────────────
@@ -21445,6 +21832,11 @@ async function createAgentServices(input) {
21445
21832
  taskAware: config.Sage?.inject?.taskAware,
21446
21833
  minScore: config.Sage?.inject?.minScore,
21447
21834
  minImportance: config.Sage?.inject?.minImportance,
21835
+ // Forward the explicit relation floor so an operator-configured
21836
+ // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
21837
+ // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
21838
+ // is the CLI default but masks operator overrides.
21839
+ relationFloor: config.Sage?.inject?.relationFloor,
21448
21840
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
21449
21841
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
21450
21842
  triggers: config.Sage?.inject?.triggers,
@@ -21466,6 +21858,10 @@ async function createAgentServices(input) {
21466
21858
  maxMemories: config.Sage?.inject?.maxTurnMemories,
21467
21859
  maxChars: config.Sage?.inject?.maxCharsPerTurn,
21468
21860
  minScore: config.Sage?.inject?.minScore,
21861
+ // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
21862
+ // value drives both runtimes instead of silently falling back to the
21863
+ // 0.3 default. The undefined case keeps the middleware's own default.
21864
+ metadataWeight: config.Sage?.retrieval?.metadataWeight,
21469
21865
  getSessionId: getSageSessionId,
21470
21866
  tracker: sageInjectionTracker
21471
21867
  })
@@ -21490,25 +21886,29 @@ async function createAgentServices(input) {
21490
21886
  strategy: config.context?.strategy,
21491
21887
  preserveK: config.context?.preserveK ?? 10,
21492
21888
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
21889
+ // Match the CLI/TUI runtime: keep corrections, errors and decisions
21890
+ // verbatim while collapsing routine assistant chatter/tool protocol.
21891
+ // Without this WebUI's hybrid strategy builds an ever-growing lossless
21892
+ // digest and eventually relies on blunt emergency head/tail trimming.
21893
+ smart: true,
21493
21894
  summarizerModel: config.context?.summarizerModel,
21494
21895
  llmSelector: config.context?.llmSelector
21495
21896
  });
21496
21897
  const initialContextPolicy = resolveContextWindowPolicy2(config.context);
21497
21898
  let autoCompactor;
21498
21899
  if (config.context?.autoCompact !== false) {
21499
- let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
21500
- if (!effectiveMaxContext) {
21501
- try {
21502
- const m = await resolveProviderModelMetadata(
21503
- modelsRegistry,
21504
- config.provider,
21505
- context.model,
21506
- config.providers?.[config.provider]
21507
- );
21508
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
21509
- } catch {
21510
- }
21900
+ let effectiveMaxContext = 0;
21901
+ try {
21902
+ const m = await resolveProviderModelMetadata(
21903
+ modelsRegistry,
21904
+ config.provider,
21905
+ context.model,
21906
+ config.providers?.[config.provider]
21907
+ );
21908
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
21909
+ } catch {
21511
21910
  }
21911
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
21512
21912
  if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
21513
21913
  autoCompactor = new AutoCompactionMiddlewareCtor(
21514
21914
  compactor,
@@ -22130,16 +22530,24 @@ function createMessageDispatcher(opts) {
22130
22530
  getAgent: () => deps2.agent,
22131
22531
  getSessionId: () => state.getSession().id,
22132
22532
  runControl: {
22133
- begin: () => {
22533
+ begin: (_ws, sessionId) => {
22134
22534
  if (runLock.get()) return void 0;
22135
22535
  const controller = new AbortController();
22136
22536
  runLock.set(controller);
22537
+ runLock.setSession(sessionId);
22137
22538
  return controller;
22138
22539
  },
22139
- end: (_ws, controller) => {
22140
- if (runLock.get() === controller) runLock.set(null);
22540
+ end: (_ws, _sessionId, controller) => {
22541
+ if (runLock.get() === controller) {
22542
+ runLock.set(null);
22543
+ runLock.setSession(null);
22544
+ }
22141
22545
  },
22142
- abort: () => runLock.get()?.abort()
22546
+ abort: (_ws, sessionId) => {
22547
+ if (runLock.getSession() === sessionId || !runLock.getSession()) {
22548
+ runLock.get()?.abort();
22549
+ }
22550
+ }
22143
22551
  },
22144
22552
  pendingConfirms,
22145
22553
  send,
@@ -22161,10 +22569,20 @@ function createMessageDispatcher(opts) {
22161
22569
  const goalSnapshotRoutes = {
22162
22570
  getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
22163
22571
  };
22572
+ const kanbanSupervisor = createKanbanSupervisor({
22573
+ projectRoot: () => state.getProjectRoot(),
22574
+ broadcast: (message) => broadcast(state.getClients(), message),
22575
+ log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
22576
+ });
22577
+ if (opts.onDispose) {
22578
+ const dispose = () => kanbanSupervisor.dispose();
22579
+ opts.onDispose(dispose);
22580
+ }
22164
22581
  const kanbanContext = () => ({
22165
22582
  projectRoot: state.getProjectRoot(),
22166
22583
  context: deps2.context,
22167
- broadcast: (message) => broadcast(state.getClients(), message)
22584
+ broadcast: (message) => broadcast(state.getClients(), message),
22585
+ supervisor: kanbanSupervisor
22168
22586
  });
22169
22587
  const kanbanHostRoutes = {
22170
22588
  meta: async (ws) => {
@@ -22680,6 +23098,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
22680
23098
  transition = transition.then(async () => {
22681
23099
  if (stopped) return;
22682
23100
  if (pendingClaim?.sessionId === sessionId) {
23101
+ await pendingClaim.claim.activate({
23102
+ sessionId,
23103
+ ...target,
23104
+ clientType: "webui",
23105
+ pid: process.pid,
23106
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
23107
+ agents: statusTracker.getAgents()
23108
+ });
22683
23109
  pendingClaim = void 0;
22684
23110
  } else {
22685
23111
  await register(sessionId, true, target);
@@ -22703,14 +23129,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
22703
23129
  }
22704
23130
  if (sessionId === activeSessionId) return async () => {
22705
23131
  };
22706
- const previousSessionId = activeSessionId;
22707
- const previousTarget = activeTarget;
22708
23132
  const token = Symbol(sessionId);
22709
- await register(sessionId, true, target);
22710
- pendingClaim = { sessionId, previousSessionId, token };
23133
+ if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
23134
+ const reservation = await registry.reserveResume({
23135
+ sessionId,
23136
+ projectSlug: target.projectSlug,
23137
+ projectRoot: target.projectRoot
23138
+ });
23139
+ pendingClaim = { sessionId, token, claim: reservation, target };
23140
+ } else {
23141
+ await register(sessionId, true, target);
23142
+ pendingClaim = {
23143
+ sessionId,
23144
+ token,
23145
+ target,
23146
+ claim: {
23147
+ reservation: {
23148
+ reservationId: "legacy",
23149
+ targetSessionId: sessionId,
23150
+ requesterInstanceId: "legacy",
23151
+ expiresAt: Number.MAX_SAFE_INTEGER
23152
+ },
23153
+ activate: async () => void 0,
23154
+ cancel: async () => register(activeSessionId, true, activeTarget)
23155
+ }
23156
+ };
23157
+ }
22711
23158
  return async () => {
22712
23159
  if (pendingClaim?.token !== token) return;
22713
- await register(previousSessionId, true, previousTarget);
23160
+ await pendingClaim.claim.cancel();
22714
23161
  pendingClaim = void 0;
22715
23162
  };
22716
23163
  };
@@ -23637,10 +24084,11 @@ function startHttpServer(opts) {
23637
24084
  return httpServer;
23638
24085
  }
23639
24086
  function registerShutdown(deps2) {
23640
- registerShutdownHandlers({
24087
+ return registerShutdownHandlers({
23641
24088
  flushSession: deps2.flushSession,
23642
24089
  clients: deps2.clients,
23643
24090
  servers: deps2.servers,
24091
+ onPreShutdown: deps2.onPreShutdown,
23644
24092
  onShutdown: deps2.onShutdown
23645
24093
  });
23646
24094
  }
@@ -23910,7 +24358,7 @@ async function startWebUI(opts = {}) {
23910
24358
  if (events.listenerCount("tool.confirm_needed") === 0) {
23911
24359
  throw new Error("No permission confirmation surface is connected");
23912
24360
  }
23913
- const decision = await new Promise((resolve15) => {
24361
+ const decision = await new Promise((resolve16) => {
23914
24362
  events.emit("tool.confirm_needed", {
23915
24363
  sessionId: context.session.id,
23916
24364
  tool: confirmTool,
@@ -23920,7 +24368,7 @@ async function startWebUI(opts = {}) {
23920
24368
  decisionSource: pending.decisionSource,
23921
24369
  riskTier: pending.riskTier,
23922
24370
  boundaryReason: pending.boundaryReason,
23923
- resolve: resolve15
24371
+ resolve: resolve16
23924
24372
  });
23925
24373
  });
23926
24374
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -23955,10 +24403,15 @@ async function startWebUI(opts = {}) {
23955
24403
  );
23956
24404
  }
23957
24405
  let _runLock = null;
24406
+ let _runLockSession = null;
23958
24407
  const runLockControl = {
23959
24408
  get: () => _runLock,
23960
24409
  set: (ctrl) => {
23961
24410
  _runLock = ctrl;
24411
+ },
24412
+ getSession: () => _runLockSession,
24413
+ setSession: (id) => {
24414
+ _runLockSession = id;
23962
24415
  }
23963
24416
  };
23964
24417
  const pendingConfirms = /* @__PURE__ */ new Map();
@@ -24083,6 +24536,7 @@ async function startWebUI(opts = {}) {
24083
24536
  if (ctrl) {
24084
24537
  ctrl.abort();
24085
24538
  runLockControl.set(null);
24539
+ runLockControl.setSession(null);
24086
24540
  }
24087
24541
  },
24088
24542
  isRunActive: () => runLockControl.get() !== null,
@@ -24232,6 +24686,7 @@ async function startWebUI(opts = {}) {
24232
24686
  })
24233
24687
  });
24234
24688
  const routes = buildRoutes(state, deps2, cb);
24689
+ let kanbanSupervisorDispose = null;
24235
24690
  const handleMessage = createMessageDispatcher({
24236
24691
  state,
24237
24692
  deps: deps2,
@@ -24239,7 +24694,10 @@ async function startWebUI(opts = {}) {
24239
24694
  promptsCtx,
24240
24695
  codebaseIndexing,
24241
24696
  runLock: runLockControl,
24242
- pendingConfirms
24697
+ pendingConfirms,
24698
+ onDispose: (dispose) => {
24699
+ kanbanSupervisorDispose = dispose;
24700
+ }
24243
24701
  });
24244
24702
  const mailbox = getSharedProjectMailbox4(
24245
24703
  resolveProjectDir3(context.projectRoot, wstackGlobalRoot2()),
@@ -24273,7 +24731,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24273
24731
  priority: "high",
24274
24732
  senderSessionId: session.id
24275
24733
  }).catch((err) => {
24276
- console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
24734
+ console.warn(
24735
+ JSON.stringify({
24736
+ level: "warn",
24737
+ event: "webui.security_rejection_mailbox_note_failed",
24738
+ message: String(err),
24739
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
24740
+ })
24741
+ );
24277
24742
  });
24278
24743
  },
24279
24744
  goalHandler,
@@ -24287,7 +24752,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24287
24752
  });
24288
24753
  wssPrimary.on("connection", handleConnection);
24289
24754
  if (wssSecondary) wssSecondary.on("connection", handleConnection);
24290
- registerShutdown({
24755
+ let unregisterShutdown = () => {
24756
+ };
24757
+ unregisterShutdown = registerShutdown({
24291
24758
  flushSession: async () => {
24292
24759
  await session.append({
24293
24760
  type: "session_end",
@@ -24303,7 +24770,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24303
24770
  wssPrimary,
24304
24771
  ...wssSecondary ? [wssSecondary] : []
24305
24772
  ],
24773
+ onPreShutdown: () => {
24774
+ kanbanSupervisorDispose?.();
24775
+ kanbanSupervisorDispose = null;
24776
+ },
24306
24777
  onShutdown: async () => {
24778
+ unregisterShutdown();
24307
24779
  await todosCheckpoint.detach();
24308
24780
  await stopHeapWatchdog();
24309
24781
  credentialWatcherClose?.();