@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.
package/dist/index.js CHANGED
@@ -4409,7 +4409,8 @@ function createConversationOperations(ctx) {
4409
4409
  userMessage: async (ws, msg) => {
4410
4410
  if (!ensureCurrentSession(ws, msg, "user_message")) return;
4411
4411
  const payload = msg.payload ?? {};
4412
- const controller = ctx.runControl.begin(ws);
4412
+ const originSessionId = ctx.getSessionId();
4413
+ const controller = ctx.runControl.begin(ws, originSessionId);
4413
4414
  if (!controller) {
4414
4415
  ctx.send(ws, {
4415
4416
  type: "error",
@@ -4420,7 +4421,6 @@ function createConversationOperations(ctx) {
4420
4421
  });
4421
4422
  return;
4422
4423
  }
4423
- const originSessionId = ctx.getSessionId();
4424
4424
  try {
4425
4425
  const agent = ctx.getAgent();
4426
4426
  if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
@@ -4479,12 +4479,13 @@ function createConversationOperations(ctx) {
4479
4479
  });
4480
4480
  }
4481
4481
  } finally {
4482
- ctx.runControl.end(ws, controller);
4482
+ ctx.runControl.end(ws, originSessionId, controller);
4483
4483
  }
4484
4484
  },
4485
4485
  abort: (ws, msg) => {
4486
4486
  if (!ensureCurrentSession(ws, msg, "abort")) return;
4487
- ctx.runControl.abort(ws);
4487
+ const sessionId = requestedSessionId(msg) ?? ctx.getSessionId();
4488
+ ctx.runControl.abort(ws, sessionId);
4488
4489
  ctx.notifyAbort(ws, {
4489
4490
  type: "error",
4490
4491
  payload: sessionPayload2({ phase: "abort", message: "User aborted" })
@@ -5536,6 +5537,7 @@ function createConnectionLifecycle(options) {
5536
5537
  }
5537
5538
 
5538
5539
  // src/server/connections-health-route.ts
5540
+ import * as net from "node:net";
5539
5541
  import {
5540
5542
  ChronicleProjectServerClient,
5541
5543
  createChronicleProjectAccess as createChronicleProjectAccess2,
@@ -5545,13 +5547,22 @@ import {
5545
5547
  isMailboxProjectServerAvailable,
5546
5548
  MailboxProjectServerConnection
5547
5549
  } from "@wrongstack/core/coordination";
5550
+ import { SessionCatalogProjectClient } from "@wrongstack/core/session-catalog";
5548
5551
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
5549
5552
  import {
5550
5553
  closeKanbanServerConnections,
5551
5554
  getKanbanServerConnection,
5552
5555
  isKanbanServerAvailable
5553
5556
  } from "@wrongstack/kanban";
5554
- import * as net from "node:net";
5557
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
5558
+ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
5559
+ import {
5560
+ checkCodebaseIndexServerHealth,
5561
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
5562
+ getIndexState,
5563
+ resolveProjectIndexDaemonAvailability,
5564
+ shutdownCodebaseIndexServer
5565
+ } from "@wrongstack/tools";
5555
5566
 
5556
5567
  // src/server/privileged-actions.ts
5557
5568
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -5585,15 +5596,6 @@ async function authorizeWebUIAction(boundary, action, logger) {
5585
5596
  }
5586
5597
 
5587
5598
  // 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
5599
  async function handleConnectionsHealthRoute(context, ws, message) {
5598
5600
  if (message.type !== "connections.health") return false;
5599
5601
  try {
@@ -5614,6 +5616,7 @@ async function handleConnectionsHealthRoute(context, ws, message) {
5614
5616
  async function collectConnectionsHealth(options) {
5615
5617
  const services = await Promise.all([
5616
5618
  Promise.resolve(webuiHealth(options.backend)),
5619
+ sessionCatalogHealth(options.projectRoot),
5617
5620
  chronicleHealth(options.projectRoot),
5618
5621
  codebaseIndexHealth(options.projectRoot, options.indexDir),
5619
5622
  sageHealth(options.projectRoot),
@@ -5631,6 +5634,50 @@ async function collectConnectionsHealth(options) {
5631
5634
  services
5632
5635
  };
5633
5636
  }
5637
+ async function sessionCatalogHealth(projectRoot) {
5638
+ const startedAt = Date.now();
5639
+ try {
5640
+ const paths = resolveWstackPaths2({ projectRoot });
5641
+ const client = new SessionCatalogProjectClient({
5642
+ projectDir: paths.projectDir,
5643
+ projectRoot
5644
+ });
5645
+ try {
5646
+ const health = await client.ping();
5647
+ return {
5648
+ id: "session-catalog",
5649
+ label: "Session Catalog",
5650
+ status: health.damagedRows > 0 ? "degraded" : "healthy",
5651
+ required: true,
5652
+ mode: "project-daemon",
5653
+ 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).`,
5654
+ ownerPid: health.pid,
5655
+ endpoint: health.endpoint,
5656
+ storage: health.databasePath,
5657
+ uptimeMs: health.uptimeMs,
5658
+ latencyMs: Date.now() - startedAt,
5659
+ clients: health.clients,
5660
+ activeRequests: health.activeRequests,
5661
+ queuedWork: health.reservations + health.maintenanceLeases,
5662
+ control: "none"
5663
+ };
5664
+ } finally {
5665
+ await client.close().catch(() => void 0);
5666
+ }
5667
+ } catch (error2) {
5668
+ return {
5669
+ id: "session-catalog",
5670
+ label: "Session Catalog",
5671
+ status: "error",
5672
+ required: true,
5673
+ mode: "project-daemon",
5674
+ detail: "Project-scoped session ownership and catalog are unavailable.",
5675
+ latencyMs: Date.now() - startedAt,
5676
+ lastError: error2 instanceof Error ? error2.message : String(error2),
5677
+ control: "none"
5678
+ };
5679
+ }
5680
+ }
5634
5681
  function webuiHealth(backend) {
5635
5682
  return {
5636
5683
  id: "webui",
@@ -6229,7 +6276,11 @@ async function restartSageServer(projectRoot) {
6229
6276
  });
6230
6277
  const verifyConn = new SageProjectServerConnection(projectRoot);
6231
6278
  try {
6232
- await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
6279
+ await verifyConn.call(
6280
+ "ping",
6281
+ {},
6282
+ { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } }
6283
+ );
6233
6284
  return {
6234
6285
  serviceId: "sage",
6235
6286
  action: "restart",
@@ -6463,28 +6514,28 @@ async function restartMailboxServer(projectRoot) {
6463
6514
  var RESTART_POLL_INTERVAL_MS = 250;
6464
6515
  var RESTART_DEADLINE_MS = 3e3;
6465
6516
  function isEndpointAlive(endpoint) {
6466
- return new Promise((resolve16) => {
6517
+ return new Promise((resolve17) => {
6467
6518
  const sock = net.createConnection(endpoint);
6468
6519
  const timer = setTimeout(() => {
6469
6520
  sock.destroy();
6470
- resolve16(false);
6521
+ resolve17(false);
6471
6522
  }, 500);
6472
6523
  timer.unref?.();
6473
6524
  sock.once("connect", () => {
6474
6525
  clearTimeout(timer);
6475
6526
  sock.destroy();
6476
- resolve16(true);
6527
+ resolve17(true);
6477
6528
  });
6478
6529
  sock.once("error", () => {
6479
6530
  clearTimeout(timer);
6480
6531
  sock.destroy();
6481
- resolve16(false);
6532
+ resolve17(false);
6482
6533
  });
6483
6534
  });
6484
6535
  }
6485
6536
  async function waitForShutdown(probe) {
6486
6537
  if (!probe) {
6487
- await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6538
+ await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
6488
6539
  return;
6489
6540
  }
6490
6541
  const deadline = Date.now() + RESTART_DEADLINE_MS;
@@ -6495,7 +6546,7 @@ async function waitForShutdown(probe) {
6495
6546
  } catch {
6496
6547
  return;
6497
6548
  }
6498
- await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6549
+ await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
6499
6550
  }
6500
6551
  }
6501
6552
  function failureService(id, label, required, mode, error2, latencyMs) {
@@ -6658,9 +6709,9 @@ async function handleGitInfo(ws, projectRoot) {
6658
6709
  const cwd = projectRoot || void 0;
6659
6710
  try {
6660
6711
  const { execFile: ef } = await import("node:child_process");
6661
- const git = (args) => new Promise((resolve16) => {
6712
+ const git = (args) => new Promise((resolve17) => {
6662
6713
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
6663
- resolve16(err ? "" : stdout.trim());
6714
+ resolve17(err ? "" : stdout.trim());
6664
6715
  });
6665
6716
  });
6666
6717
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -6686,12 +6737,12 @@ async function handleGitInfo(ws, projectRoot) {
6686
6737
  function makeGit(cwd) {
6687
6738
  return async (args) => {
6688
6739
  const { execFile: ef } = await import("node:child_process");
6689
- return new Promise((resolve16) => {
6740
+ return new Promise((resolve17) => {
6690
6741
  ef(
6691
6742
  "git",
6692
6743
  args,
6693
6744
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
6694
- (err, stdout) => resolve16(err ? "" : stdout)
6745
+ (err, stdout) => resolve17(err ? "" : stdout)
6695
6746
  );
6696
6747
  });
6697
6748
  };
@@ -6856,7 +6907,7 @@ import { execFile } from "node:child_process";
6856
6907
  var GIT_TIMEOUT_MS = 1e4;
6857
6908
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
6858
6909
  function gitStdout(cwd, args) {
6859
- return new Promise((resolve16) => {
6910
+ return new Promise((resolve17) => {
6860
6911
  execFile(
6861
6912
  "git",
6862
6913
  [...args],
@@ -6867,7 +6918,7 @@ function gitStdout(cwd, args) {
6867
6918
  timeout: GIT_TIMEOUT_MS,
6868
6919
  maxBuffer: GIT_MAX_OUTPUT_BYTES
6869
6920
  },
6870
- (error2, stdout) => resolve16(error2 ? null : stdout)
6921
+ (error2, stdout) => resolve17(error2 ? null : stdout)
6871
6922
  );
6872
6923
  });
6873
6924
  }
@@ -7158,14 +7209,14 @@ var GoalWebSocketHandler = class {
7158
7209
  const cwd = env?.cwd ?? this.projectRoot;
7159
7210
  try {
7160
7211
  const { execFile: execFile2 } = await import("node:child_process");
7161
- const result = await new Promise((resolve16) => {
7212
+ const result = await new Promise((resolve17) => {
7162
7213
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7163
7214
  execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7164
7215
  if (err && err.code === "ENOENT") {
7165
- resolve16("[verify] tsc not found \u2014 skipping");
7216
+ resolve17("[verify] tsc not found \u2014 skipping");
7166
7217
  return;
7167
7218
  }
7168
- resolve16(stdout + stderr);
7219
+ resolve17(stdout + stderr);
7169
7220
  });
7170
7221
  });
7171
7222
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -7903,7 +7954,7 @@ function pushEvent(event) {
7903
7954
  }
7904
7955
  }
7905
7956
  function parseBody(req) {
7906
- return new Promise((resolve16, reject) => {
7957
+ return new Promise((resolve17, reject) => {
7907
7958
  let body = "";
7908
7959
  let bodyBytes = 0;
7909
7960
  let tooLarge = false;
@@ -7923,7 +7974,7 @@ function parseBody(req) {
7923
7974
  return;
7924
7975
  }
7925
7976
  try {
7926
- resolve16(JSON.parse(body));
7977
+ resolve17(JSON.parse(body));
7927
7978
  } catch {
7928
7979
  reject(new Error("Invalid JSON"));
7929
7980
  }
@@ -8018,7 +8069,7 @@ import * as path10 from "node:path";
8018
8069
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
8019
8070
  var MAX_BODY_BYTES = 10 * 1024 * 1024;
8020
8071
  function readJsonBody(req) {
8021
- return new Promise((resolve16, reject) => {
8072
+ return new Promise((resolve17, reject) => {
8022
8073
  const chunks = [];
8023
8074
  let total = 0;
8024
8075
  req.on("data", (chunk) => {
@@ -8030,7 +8081,7 @@ function readJsonBody(req) {
8030
8081
  }
8031
8082
  chunks.push(chunk);
8032
8083
  });
8033
- req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
8084
+ req.on("end", () => resolve17(Buffer.concat(chunks).toString("utf8")));
8034
8085
  req.on("error", (err) => reject(err));
8035
8086
  });
8036
8087
  }
@@ -8248,8 +8299,8 @@ async function handleApiSessions(res, globalRoot) {
8248
8299
  return;
8249
8300
  }
8250
8301
  try {
8251
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8252
- const registry = new SessionRegistry(globalRoot);
8302
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8303
+ const registry = getSessionRegistry3(globalRoot);
8253
8304
  const sessions = await registry.list();
8254
8305
  const result = sessions.map((s) => ({
8255
8306
  sessionId: s.sessionId,
@@ -8286,8 +8337,8 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8286
8337
  return;
8287
8338
  }
8288
8339
  try {
8289
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8290
- const registry = new SessionRegistry(globalRoot);
8340
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8341
+ const registry = getSessionRegistry3(globalRoot);
8291
8342
  const entry = await registry.get(sessionId);
8292
8343
  if (!entry) {
8293
8344
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8295,20 +8346,22 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
8295
8346
  return;
8296
8347
  }
8297
8348
  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
- }));
8349
+ res.end(
8350
+ JSON.stringify({
8351
+ sessionId: entry.sessionId,
8352
+ projectName: entry.projectName,
8353
+ status: entry.status,
8354
+ agents: entry.agents.map((a) => ({
8355
+ id: a.id,
8356
+ name: a.name,
8357
+ status: a.status,
8358
+ currentTool: a.currentTool,
8359
+ iterations: a.iterations,
8360
+ toolCalls: a.toolCalls,
8361
+ lastActivityAt: a.lastActivityAt
8362
+ }))
8363
+ })
8364
+ );
8312
8365
  } catch (err) {
8313
8366
  res.writeHead(500, { "Content-Type": "application/json" });
8314
8367
  res.end(JSON.stringify({ error: sanitizeApiError(err) }));
@@ -8426,9 +8479,9 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8426
8479
  return;
8427
8480
  }
8428
8481
  try {
8429
- const { SessionRegistry, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8482
+ const { getSessionRegistry: getSessionRegistry3, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
8430
8483
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8431
- const registry = new SessionRegistry(globalRoot);
8484
+ const registry = getSessionRegistry3(globalRoot);
8432
8485
  const entry = await registry.get(sessionId);
8433
8486
  if (!entry) {
8434
8487
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8436,7 +8489,10 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8436
8489
  return;
8437
8490
  }
8438
8491
  const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
8439
- const store = new DefaultSessionStore4({ dir: paths.projectSessions });
8492
+ const store = new DefaultSessionStore4({
8493
+ dir: paths.projectSessions,
8494
+ projectRoot: entry.projectRoot
8495
+ });
8440
8496
  const reader = new DefaultSessionReader2({ store });
8441
8497
  const RING = Math.max(limit * 4, 2e3);
8442
8498
  const ring = [];
@@ -8476,7 +8532,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8476
8532
  }
8477
8533
  }
8478
8534
  function readJsonBody2(req) {
8479
- return new Promise((resolve16, reject) => {
8535
+ return new Promise((resolve17, reject) => {
8480
8536
  const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8481
8537
  if (contentType !== "application/json") {
8482
8538
  reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
@@ -8492,7 +8548,7 @@ function readJsonBody2(req) {
8492
8548
  });
8493
8549
  req.on("end", () => {
8494
8550
  try {
8495
- resolve16(data ? JSON.parse(data) : {});
8551
+ resolve17(data ? JSON.parse(data) : {});
8496
8552
  } catch (err) {
8497
8553
  reject(err instanceof Error ? err : new Error(String(err)));
8498
8554
  }
@@ -8528,10 +8584,10 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
8528
8584
  const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
8529
8585
  const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
8530
8586
  try {
8531
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8587
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8532
8588
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8533
8589
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8534
- const registry = new SessionRegistry(globalRoot);
8590
+ const registry = getSessionRegistry3(globalRoot);
8535
8591
  const entry = await registry.get(sessionId);
8536
8592
  if (!entry) {
8537
8593
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8556,10 +8612,10 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
8556
8612
  return;
8557
8613
  }
8558
8614
  try {
8559
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8615
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8560
8616
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8561
8617
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8562
- const registry = new SessionRegistry(globalRoot);
8618
+ const registry = getSessionRegistry3(globalRoot);
8563
8619
  const entry = await registry.get(sessionId);
8564
8620
  if (!entry) {
8565
8621
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8616,10 +8672,10 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
8616
8672
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
8617
8673
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8618
8674
  try {
8619
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8675
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8620
8676
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8621
8677
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8622
- const registry = new SessionRegistry(globalRoot);
8678
+ const registry = getSessionRegistry3(globalRoot);
8623
8679
  const entry = await registry.get(sessionId);
8624
8680
  if (!entry) {
8625
8681
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -8665,10 +8721,10 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
8665
8721
  }
8666
8722
  const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
8667
8723
  try {
8668
- const { SessionRegistry } = await import("@wrongstack/core/storage");
8724
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
8669
8725
  const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
8670
8726
  const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
8671
- const registry = new SessionRegistry(globalRoot);
8727
+ const registry = getSessionRegistry3(globalRoot);
8672
8728
  const all = await registry.list();
8673
8729
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
8674
8730
  const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
@@ -8768,14 +8824,14 @@ async function readJsonBody3(res, req) {
8768
8824
  });
8769
8825
  return null;
8770
8826
  }
8771
- return new Promise((resolve16) => {
8827
+ return new Promise((resolve17) => {
8772
8828
  let data = "";
8773
8829
  let failed = false;
8774
8830
  const fail2 = (message) => {
8775
8831
  if (failed) return;
8776
8832
  failed = true;
8777
8833
  sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
8778
- resolve16(null);
8834
+ resolve17(null);
8779
8835
  };
8780
8836
  req.on("data", (chunk) => {
8781
8837
  if (failed) return;
@@ -8788,7 +8844,7 @@ async function readJsonBody3(res, req) {
8788
8844
  req.on("end", () => {
8789
8845
  if (failed) return;
8790
8846
  try {
8791
- resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
8847
+ resolve17(data.trim().length === 0 ? {} : JSON.parse(data));
8792
8848
  } catch {
8793
8849
  fail2("Request body is not valid JSON");
8794
8850
  }
@@ -9627,7 +9683,7 @@ function strictDecodeParam(segment, res) {
9627
9683
  function createHttpServer(opts) {
9628
9684
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
9629
9685
  const distDir = path13.resolve(opts.distDir);
9630
- const requireAccessToken = true;
9686
+ const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
9631
9687
  const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
9632
9688
  const trustedHostnames = (() => {
9633
9689
  const names = [...opts.allowedHostnames ?? []];
@@ -10173,10 +10229,78 @@ function createHttpServer(opts) {
10173
10229
  }
10174
10230
 
10175
10231
  // src/server/instance-registry.ts
10232
+ import * as fs11 from "node:fs/promises";
10176
10233
  import * as os from "node:os";
10177
10234
  import * as path14 from "node:path";
10178
- import * as fs11 from "node:fs/promises";
10179
10235
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
10236
+ function normalizeRoot(root) {
10237
+ const resolved = path14.resolve(root);
10238
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
10239
+ }
10240
+ function isLiveSessionStatus(status) {
10241
+ return status === "active" || status === "idle";
10242
+ }
10243
+ function instanceRole(instance) {
10244
+ return instance.role ?? "standalone";
10245
+ }
10246
+ function resolveAttachability(input) {
10247
+ const { session, instance } = input;
10248
+ if (!isLiveSessionStatus(session.status)) {
10249
+ return { attachable: false, degradedReason: "session-not-live" };
10250
+ }
10251
+ if (!instance) {
10252
+ return { attachable: false, degradedReason: "live-session-no-webui-endpoint" };
10253
+ }
10254
+ if (instance.pid !== session.pid) {
10255
+ return { attachable: false, degradedReason: "endpoint-owner-mismatch" };
10256
+ }
10257
+ if (!instance.sessionId) {
10258
+ return { attachable: false, instance, degradedReason: "endpoint-missing-session-id" };
10259
+ }
10260
+ if (instance.sessionId !== session.sessionId) {
10261
+ return { attachable: false, instance, degradedReason: "endpoint-session-mismatch" };
10262
+ }
10263
+ if (instanceRole(instance) !== "session-child") {
10264
+ return { attachable: false, instance, degradedReason: "endpoint-not-session-child" };
10265
+ }
10266
+ if (instance.attachable === false) {
10267
+ return { attachable: false, instance, degradedReason: "endpoint-not-attachable" };
10268
+ }
10269
+ return {
10270
+ attachable: true,
10271
+ endpoint: {
10272
+ host: instance.host,
10273
+ httpPort: instance.httpPort,
10274
+ url: instance.url,
10275
+ ...instance.authToken ? { authToken: instance.authToken } : {}
10276
+ }
10277
+ };
10278
+ }
10279
+ function joinSessionRegistryWithWebUIInstances(input) {
10280
+ const targetRoot = input.projectRoot ? normalizeRoot(input.projectRoot) : void 0;
10281
+ const sessions = input.sessions.filter((session) => {
10282
+ if (input.projectSlug && session.projectSlug !== input.projectSlug) return false;
10283
+ if (targetRoot && normalizeRoot(session.projectRoot) !== targetRoot) return false;
10284
+ return true;
10285
+ });
10286
+ return sessions.map((session) => {
10287
+ const instance = input.instances.find((candidate) => candidate.sessionId === session.sessionId) ?? input.instances.find(
10288
+ (candidate) => candidate.pid === session.pid && normalizeRoot(candidate.projectRoot) === normalizeRoot(session.projectRoot)
10289
+ );
10290
+ const resolved = resolveAttachability({ session, instance });
10291
+ return {
10292
+ sessionId: session.sessionId,
10293
+ projectRoot: session.projectRoot,
10294
+ workingDir: session.workingDir,
10295
+ sessionPid: session.pid,
10296
+ status: session.status,
10297
+ ...instance ? { instance } : {},
10298
+ ...resolved.endpoint ? { endpoint: resolved.endpoint } : {},
10299
+ attachable: resolved.attachable,
10300
+ ...resolved.degradedReason ? { degradedReason: resolved.degradedReason } : {}
10301
+ };
10302
+ });
10303
+ }
10180
10304
  function defaultBaseDir() {
10181
10305
  return path14.join(os.homedir(), ".wrongstack");
10182
10306
  }
@@ -11236,6 +11360,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11236
11360
  fail(ws, type, `Board not found: ${boardId}`);
11237
11361
  return true;
11238
11362
  }
11363
+ if (ctx.supervisor) {
11364
+ const snapshots = await ctx.supervisor.auditNow(boardId);
11365
+ const snapshot = snapshots[0];
11366
+ if (snapshot) {
11367
+ ok(ws, type, snapshot);
11368
+ return true;
11369
+ }
11370
+ }
11239
11371
  const reconciled = await reconcileKanbanBoard(ctx.projectRoot, boardId);
11240
11372
  let health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11241
11373
  const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments(ctx.projectRoot, boardId, {
@@ -12000,6 +12132,15 @@ function createShutdown(res) {
12000
12132
  } catch {
12001
12133
  }
12002
12134
  }
12135
+ if (res.onPreShutdown) {
12136
+ try {
12137
+ await res.onPreShutdown();
12138
+ } catch (e) {
12139
+ log(
12140
+ `[WebUI] Error during pre-shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`
12141
+ );
12142
+ }
12143
+ }
12003
12144
  for (const server of res.servers) server?.close();
12004
12145
  if (res.onShutdown) {
12005
12146
  try {
@@ -13723,16 +13864,16 @@ function getSurfaceDefaultPorts(surface) {
13723
13864
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
13724
13865
  }
13725
13866
  function isPortFree(host, port) {
13726
- return new Promise((resolve16) => {
13867
+ return new Promise((resolve17) => {
13727
13868
  const srv = net2.createServer();
13728
- srv.once("error", () => resolve16(false));
13869
+ srv.once("error", () => resolve17(false));
13729
13870
  srv.once("listening", () => {
13730
- srv.close(() => resolve16(true));
13871
+ srv.close(() => resolve17(true));
13731
13872
  });
13732
13873
  try {
13733
13874
  srv.listen(port, host);
13734
13875
  } catch {
13735
- resolve16(false);
13876
+ resolve17(false);
13736
13877
  }
13737
13878
  });
13738
13879
  }
@@ -13888,7 +14029,7 @@ async function startStaticServe(opts, deps2 = {}) {
13888
14029
  return { server, port: opts.httpPort };
13889
14030
  }
13890
14031
  function runPnpmBuild(cwd, workspace, timeoutMs) {
13891
- return new Promise((resolve16, reject) => {
14032
+ return new Promise((resolve17, reject) => {
13892
14033
  const child = spawn2("pnpm", ["--filter", workspace, "build"], {
13893
14034
  cwd,
13894
14035
  shell: process.platform === "win32",
@@ -13906,7 +14047,7 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
13906
14047
  });
13907
14048
  child.once("close", (code) => {
13908
14049
  clearTimeout(timer);
13909
- if (code === 0) resolve16();
14050
+ if (code === 0) resolve17();
13910
14051
  else reject(new Error(`pnpm build exited with code ${String(code)}`));
13911
14052
  });
13912
14053
  });
@@ -13970,27 +14111,41 @@ function formatExternalAccessUrls(opts) {
13970
14111
  }
13971
14112
 
13972
14113
  // src/server/embedded-lifecycle.ts
13973
- function registerWebuiInstance(p, deps2 = {}) {
14114
+ async function registerWebuiInstance(p, deps2 = {}) {
13974
14115
  const register = deps2.registerFn ?? registerInstance;
13975
- void register(
13976
- {
13977
- pid: p.pid,
13978
- surface: p.surface,
13979
- httpPort: p.httpPort,
13980
- host: p.host,
13981
- projectRoot: p.projectRoot,
13982
- projectName: path16.basename(p.projectRoot) || p.projectRoot,
13983
- startedAt: p.startedAt,
13984
- url: buildWebUIAccessUrl({
14116
+ try {
14117
+ await register(
14118
+ {
14119
+ pid: p.pid,
14120
+ surface: p.surface,
14121
+ httpPort: p.httpPort,
13985
14122
  host: p.host,
13986
- port: p.httpPort,
13987
- publicUrl: p.publicUrl
13988
- }),
13989
- ...p.authToken ? { authToken: p.authToken } : {}
13990
- },
13991
- p.registryBaseDir
13992
- ).catch(() => {
13993
- });
14123
+ projectRoot: p.projectRoot,
14124
+ projectName: path16.basename(p.projectRoot) || p.projectRoot,
14125
+ startedAt: p.startedAt,
14126
+ url: buildWebUIAccessUrl({
14127
+ host: p.host,
14128
+ port: p.httpPort,
14129
+ publicUrl: p.publicUrl
14130
+ }),
14131
+ ...p.authToken ? { authToken: p.authToken } : {},
14132
+ ...p.role ? { role: p.role } : {},
14133
+ ...p.sessionId ? { sessionId: p.sessionId } : {},
14134
+ ...p.parentPid !== void 0 ? { parentPid: p.parentPid } : {},
14135
+ ...p.parentShellId ? { parentShellId: p.parentShellId } : {},
14136
+ ...p.runtimeId ? { runtimeId: p.runtimeId } : {},
14137
+ ...p.attachable !== void 0 ? { attachable: p.attachable } : {},
14138
+ ...p.authToken ? { auth: { scheme: "registry-token", tokenPresent: true } } : {},
14139
+ ...p.lastReadyAt ? { lastReadyAt: p.lastReadyAt } : {},
14140
+ ...p.protocolVersion !== void 0 ? { protocolVersion: p.protocolVersion } : {},
14141
+ ...p.capabilities ? { capabilities: p.capabilities } : {}
14142
+ },
14143
+ p.registryBaseDir
14144
+ );
14145
+ return true;
14146
+ } catch {
14147
+ return false;
14148
+ }
13994
14149
  }
13995
14150
  function announceWebuiReady(p) {
13996
14151
  const log = p.log ?? ((m) => console.log(m));
@@ -14028,10 +14183,10 @@ async function runBounded(work, timeoutMs, label, debug) {
14028
14183
  Promise.resolve().then(() => work()).catch((err) => {
14029
14184
  debug(`[webui-server] ${label} failed: ${err}`);
14030
14185
  }),
14031
- new Promise((resolve16) => {
14186
+ new Promise((resolve17) => {
14032
14187
  timer = setTimeout(() => {
14033
14188
  debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
14034
- resolve16();
14189
+ resolve17();
14035
14190
  }, timeoutMs);
14036
14191
  timer.unref?.();
14037
14192
  })
@@ -14064,8 +14219,8 @@ function createWebuiShutdown(res) {
14064
14219
  const unregistered = unregister(res.pid, res.registryBaseDir).catch(
14065
14220
  (err) => debug(`[webui-server] unregister failed: ${err}`)
14066
14221
  );
14067
- await new Promise((resolve16) => {
14068
- res.wss.close(() => resolve16());
14222
+ await new Promise((resolve17) => {
14223
+ res.wss.close(() => resolve17());
14069
14224
  });
14070
14225
  await unregistered;
14071
14226
  log("[WebUI] Server stopped");
@@ -14874,6 +15029,10 @@ import {
14874
15029
  recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
14875
15030
  resolveGateEnforcement
14876
15031
  } from "@wrongstack/kanban";
15032
+ function resolveProjectRoot(deps2) {
15033
+ const root = deps2.projectRoot;
15034
+ return typeof root === "function" ? root() : root;
15035
+ }
14877
15036
  var DEFAULT_INTERVAL_MS = 1e4;
14878
15037
  var MIN_INTERVAL_MS = 2e3;
14879
15038
  var DEFAULT_AGENT_COOLDOWN_MS = 5 * 6e4;
@@ -14938,14 +15097,14 @@ function createKanbanSupervisor(deps2) {
14938
15097
  publish(snapshot2);
14939
15098
  return snapshot2;
14940
15099
  }
14941
- const reconciled = await reconcileKanbanBoard2(deps2.projectRoot, board.id);
15100
+ const reconciled = await reconcileKanbanBoard2(resolveProjectRoot(deps2), board.id);
14942
15101
  const gateSwept = await sweepGateParkedTasks(deps2, reconciled?.board ?? board);
14943
- let health = await getKanbanQueueHealth2(deps2.projectRoot, { boardId: board.id });
14944
- const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(deps2.projectRoot, board.id, {
15102
+ let health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15103
+ const recovered = health.staleAssignments.count ? await recoverStaleTaskAssignments2(resolveProjectRoot(deps2), board.id, {
14945
15104
  mode: config.recoveryMode ?? "auto",
14946
15105
  reason: "Kanban supervisor found an expired worker lease."
14947
15106
  }) : null;
14948
- if (recovered) health = await getKanbanQueueHealth2(deps2.projectRoot, { boardId: board.id });
15107
+ if (recovered) health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
14949
15108
  const anomalyCount = countAnomalies(health);
14950
15109
  const snapshot = {
14951
15110
  boardId: board.id,
@@ -14964,7 +15123,7 @@ function createKanbanSupervisor(deps2) {
14964
15123
  await publishKanbanBoard(
14965
15124
  deps2.broadcast,
14966
15125
  changedBoard,
14967
- () => listBoards4(deps2.projectRoot)
15126
+ () => listBoards4(resolveProjectRoot(deps2))
14968
15127
  );
14969
15128
  }
14970
15129
  if (config.mode === "agentic" && anomalyCount > 0) {
@@ -15002,11 +15161,11 @@ function createKanbanSupervisor(deps2) {
15002
15161
  // tool-runtime boundary gate (`evaluateToolKanbanBoundary`) can resolve
15003
15162
  // the live board policy instead of failing open. Whole-board agentic
15004
15163
  // runs have no taskId, so only boardId is propagated.
15005
- context: { kanban: { boardId: board.id, projectRoot: deps2.projectRoot } },
15164
+ context: { kanban: { boardId: board.id, projectRoot: resolveProjectRoot(deps2) } },
15006
15165
  onDone: async (result) => {
15007
15166
  clearTimeout(watchdog);
15008
15167
  agentRunning.delete(board.id);
15009
- if (await getBoard3(deps2.projectRoot, board.id) === null) return;
15168
+ if (await getBoard3(resolveProjectRoot(deps2), board.id) === null) return;
15010
15169
  const current3 = snapshots.get(board.id) ?? snapshot;
15011
15170
  publish({
15012
15171
  ...current3,
@@ -15030,7 +15189,7 @@ function createKanbanSupervisor(deps2) {
15030
15189
  const auditNow = async (boardId) => {
15031
15190
  let boards;
15032
15191
  if (boardId) {
15033
- const board = await getBoard3(deps2.projectRoot, boardId);
15192
+ const board = await getBoard3(resolveProjectRoot(deps2), boardId);
15034
15193
  if (board === null) {
15035
15194
  forgetBoard(boardId);
15036
15195
  boards = [];
@@ -15038,9 +15197,9 @@ function createKanbanSupervisor(deps2) {
15038
15197
  boards = [board];
15039
15198
  }
15040
15199
  } else {
15041
- const summaries = await listBoards4(deps2.projectRoot);
15200
+ const summaries = await listBoards4(resolveProjectRoot(deps2));
15042
15201
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
15043
- boards = (await Promise.all(summaries.map((summary) => getBoard3(deps2.projectRoot, summary.id)))).filter((board) => Boolean(board));
15202
+ boards = (await Promise.all(summaries.map((summary) => getBoard3(resolveProjectRoot(deps2), summary.id)))).filter((board) => Boolean(board));
15044
15203
  }
15045
15204
  const results = [];
15046
15205
  for (const board of boards) results.push(await auditBoard(board));
@@ -15075,11 +15234,11 @@ function createKanbanSupervisor(deps2) {
15075
15234
  if (disposed) return;
15076
15235
  try {
15077
15236
  const now = Date.now();
15078
- const summaries = await listBoards4(deps2.projectRoot);
15237
+ const summaries = await listBoards4(resolveProjectRoot(deps2));
15079
15238
  pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
15080
15239
  for (const summary of summaries) {
15081
15240
  if ((nextDue.get(summary.id) ?? 0) > now) continue;
15082
- const board = await getBoard3(deps2.projectRoot, summary.id);
15241
+ const board = await getBoard3(resolveProjectRoot(deps2), summary.id);
15083
15242
  if (board) await auditBoard(board);
15084
15243
  }
15085
15244
  } catch (error2) {
@@ -15122,7 +15281,7 @@ async function sweepGateParkedTasks(deps2, board) {
15122
15281
  let lastBoard;
15123
15282
  for (const task of parked) {
15124
15283
  try {
15125
- const finalized = await finalizeTaskCompletion(deps2.projectRoot, board.id, task.id, {
15284
+ const finalized = await finalizeTaskCompletion(resolveProjectRoot(deps2), board.id, task.id, {
15126
15285
  eventContext: { actor: "kanban-supervisor" }
15127
15286
  });
15128
15287
  if (finalized) lastBoard = finalized.board;
@@ -17912,6 +18071,8 @@ function labelForEvent(e) {
17912
18071
  const count = e.messagesOmitted ?? e.messages.length;
17913
18072
  return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
17914
18073
  }
18074
+ case "messages_dropped":
18075
+ return `Oldest ${e.count} message${e.count === 1 ? "" : "s"} evicted`;
17915
18076
  case "message_truncated":
17916
18077
  return `Message truncated: ${e.before} \u2192 ${e.after}`;
17917
18078
  case "file_event":
@@ -17999,6 +18160,8 @@ function detailForEvent(e) {
17999
18160
  return `at index ${e.index}`;
18000
18161
  case "messages_replaced":
18001
18162
  return `${e.messagesOmitted ?? e.messages.length} total`;
18163
+ case "messages_dropped":
18164
+ return `dropped ${e.count} from the front`;
18002
18165
  case "message_truncated":
18003
18166
  return `truncated to ${e.after} tokens`;
18004
18167
  case "mode_changed":
@@ -18216,7 +18379,7 @@ function createSessionHandlers(ctx) {
18216
18379
  const current2 = ctx.getSession();
18217
18380
  if (current2 !== next) {
18218
18381
  try {
18219
- ctx.abortActiveRun?.();
18382
+ ctx.abortActiveRun?.(current2.id);
18220
18383
  } catch {
18221
18384
  }
18222
18385
  await finalizeSession(current2);
@@ -18803,16 +18966,17 @@ function createEmbeddedConversationRoutes(ctx) {
18803
18966
  getAgent: () => ctx.agent,
18804
18967
  getSessionId: () => ctx.agent.ctx.session?.id ?? "",
18805
18968
  runControl: {
18806
- begin: (ws) => {
18807
- if (ctx.abortControllers.has(ws)) return void 0;
18969
+ begin: (_ws, sessionId) => {
18970
+ if (ctx.abortControllers.has(sessionId)) return void 0;
18808
18971
  const controller = new AbortController();
18809
- ctx.abortControllers.set(ws, controller);
18972
+ ctx.abortControllers.set(sessionId, controller);
18810
18973
  return controller;
18811
18974
  },
18812
- end: (ws, controller) => {
18813
- if (ctx.abortControllers.get(ws) === controller) ctx.abortControllers.delete(ws);
18975
+ end: (_ws, sessionId, controller) => {
18976
+ if (ctx.abortControllers.get(sessionId) === controller)
18977
+ ctx.abortControllers.delete(sessionId);
18814
18978
  },
18815
- abort: (ws) => ctx.abortControllers.get(ws)?.abort()
18979
+ abort: (_ws, sessionId) => ctx.abortControllers.get(sessionId)?.abort()
18816
18980
  },
18817
18981
  pendingConfirms: ctx.pendingConfirms,
18818
18982
  send: ctx.send,
@@ -20234,9 +20398,13 @@ function createEmbeddedMessageRouter(deps2) {
20234
20398
  // stream from the previous session would otherwise keep running in the
20235
20399
  // background after session.new/resume. The run's own end() cleanup
20236
20400
  // removes controllers from the map when it unwinds.
20237
- abortActiveRun: () => {
20238
- for (const controller of deps2.conversationCtx.abortControllers.values()) {
20239
- controller.abort();
20401
+ abortActiveRun: (sessionId) => {
20402
+ if (sessionId) {
20403
+ deps2.conversationCtx.abortControllers.get(sessionId)?.abort();
20404
+ } else {
20405
+ for (const controller of [...deps2.conversationCtx.abortControllers.values()]) {
20406
+ controller.abort();
20407
+ }
20240
20408
  }
20241
20409
  },
20242
20410
  isRunActive: () => deps2.conversationCtx.abortControllers.size > 0
@@ -20458,8 +20626,8 @@ function createConfigWriteLock() {
20458
20626
  acquire() {
20459
20627
  const prev = lock;
20460
20628
  let release = () => void 0;
20461
- lock = new Promise((resolve16) => {
20462
- release = resolve16;
20629
+ lock = new Promise((resolve17) => {
20630
+ release = resolve17;
20463
20631
  });
20464
20632
  return { prev, release };
20465
20633
  }
@@ -21414,7 +21582,6 @@ function registerSetupEventsClientStatusWriter(deps2) {
21414
21582
  }
21415
21583
 
21416
21584
  // src/server/setup-events-fleet-broadcaster.ts
21417
- import { watch as fsWatch } from "node:fs";
21418
21585
  import * as path25 from "node:path";
21419
21586
  function registerSetupEventsFleetBroadcaster(deps2) {
21420
21587
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
@@ -21423,8 +21590,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21423
21590
  const disposers = [];
21424
21591
  const broadcastSessions = async () => {
21425
21592
  try {
21426
- const { SessionRegistry } = await import("@wrongstack/core/storage");
21427
- const registry = new SessionRegistry(globalRoot);
21593
+ const { getSessionRegistry: getSessionRegistry3 } = await import("@wrongstack/core/storage");
21594
+ const registry = getSessionRegistry3(globalRoot);
21428
21595
  const sessions = await registry.list();
21429
21596
  const ownEntry = sessions.find((s) => s.pid === process.pid);
21430
21597
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
@@ -21471,7 +21638,7 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21471
21638
  }
21472
21639
  };
21473
21640
  onFleetBroadcaster?.(broadcastSessions);
21474
- let regWatchLive = false;
21641
+ let subscriptionLive = false;
21475
21642
  let statusTimer;
21476
21643
  const scheduleStatusPoll = () => {
21477
21644
  if (isDisposed()) return;
@@ -21480,35 +21647,31 @@ function registerSetupEventsFleetBroadcaster(deps2) {
21480
21647
  void broadcastSessions();
21481
21648
  scheduleStatusPoll();
21482
21649
  },
21483
- regWatchLive ? 3e4 : 5e3
21650
+ subscriptionLive ? 3e4 : 5e3
21484
21651
  );
21485
21652
  if (statusTimer.unref) statusTimer.unref();
21486
21653
  };
21487
21654
  disposers.push(() => {
21488
21655
  if (statusTimer) clearTimeout(statusTimer);
21489
21656
  });
21490
- let regDebounce;
21491
- try {
21492
- const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
21493
- const name2 = filename ? String(filename) : "";
21494
- if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
21495
- if (regDebounce) clearTimeout(regDebounce);
21496
- regDebounce = setTimeout(() => void broadcastSessions(), 150);
21497
- });
21498
- regWatcher.on("error", () => {
21499
- regWatchLive = false;
21500
- try {
21501
- regWatcher.close();
21502
- } catch {
21503
- }
21504
- });
21505
- regWatchLive = true;
21506
- disposers.push(() => {
21507
- if (regDebounce) clearTimeout(regDebounce);
21508
- regWatcher.close();
21509
- });
21510
- } catch {
21511
- }
21657
+ let eventDebounce;
21658
+ let unsubscribe;
21659
+ void import("@wrongstack/core/storage").then(async ({ getSessionRegistry: getSessionRegistry3 }) => {
21660
+ const registry = getSessionRegistry3(globalRoot);
21661
+ const projectSlug2 = wpaths?.projectSlug;
21662
+ if (!projectSlug2 || isDisposed()) return;
21663
+ unsubscribe = await registry.subscribeProject(projectSlug2, context.projectRoot, () => {
21664
+ if (eventDebounce) clearTimeout(eventDebounce);
21665
+ eventDebounce = setTimeout(() => void broadcastSessions(), 25);
21666
+ });
21667
+ subscriptionLive = true;
21668
+ }).catch(() => {
21669
+ subscriptionLive = false;
21670
+ });
21671
+ disposers.push(() => {
21672
+ if (eventDebounce) clearTimeout(eventDebounce);
21673
+ void unsubscribe?.();
21674
+ });
21512
21675
  scheduleStatusPoll();
21513
21676
  void broadcastSessions();
21514
21677
  return () => {
@@ -21621,7 +21784,7 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
21621
21784
  }
21622
21785
 
21623
21786
  // src/server/setup-events-status-watcher.ts
21624
- import { watch as fsWatch2 } from "node:fs";
21787
+ import { watch as fsWatch } from "node:fs";
21625
21788
  import * as fs20 from "node:fs/promises";
21626
21789
  import * as path27 from "node:path";
21627
21790
 
@@ -21711,7 +21874,7 @@ function registerSetupEventsStatusWatcher(deps2) {
21711
21874
  try {
21712
21875
  await fs20.mkdir(projectsDir, { recursive: true });
21713
21876
  if (isDisposed()) return;
21714
- watcher = fsWatch2(
21877
+ watcher = fsWatch(
21715
21878
  projectsDir,
21716
21879
  { persistent: true, recursive: true },
21717
21880
  async (eventType, filename) => {
@@ -22873,7 +23036,6 @@ import {
22873
23036
  mailboxSessionTag,
22874
23037
  ObservableBrainArbiter as ObservableBrainArbiterCtor
22875
23038
  } from "@wrongstack/core/coordination";
22876
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
22877
23039
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
22878
23040
  import {
22879
23041
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -22885,6 +23047,7 @@ import {
22885
23047
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
22886
23048
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
22887
23049
  import {
23050
+ DEFAULT_TOOLS_CONFIG,
22888
23051
  resolveContextWindowPolicy as resolveContextWindowPolicy2
22889
23052
  } from "@wrongstack/core/types";
22890
23053
  import {
@@ -23045,7 +23208,7 @@ function findWorkspaceCliEntry(projectRoot) {
23045
23208
  return null;
23046
23209
  }
23047
23210
  function sleep(ms) {
23048
- return new Promise((resolve16) => setTimeout(resolve16, ms));
23211
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
23049
23212
  }
23050
23213
 
23051
23214
  // src/server/terminal-ws-handler.ts
@@ -23288,7 +23451,7 @@ function clampDim(value, fallback) {
23288
23451
  }
23289
23452
 
23290
23453
  // src/server/worktree-ws-handler.ts
23291
- import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
23454
+ import { join as join14, resolve as resolve14, sep as sep5 } from "node:path";
23292
23455
  import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
23293
23456
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
23294
23457
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -23373,11 +23536,11 @@ var WorktreeWebSocketHandler = class {
23373
23536
  // ── orphan management ─────────────────────────────────────────────────────
23374
23537
  /** Absolute managed-worktrees root for this project. */
23375
23538
  worktreesRoot() {
23376
- return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
23539
+ return resolve14(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
23377
23540
  }
23378
23541
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
23379
23542
  underRoot(dir) {
23380
- const abs = resolve13(dir);
23543
+ const abs = resolve14(dir);
23381
23544
  const root = this.worktreesRoot();
23382
23545
  return abs !== root && abs.startsWith(root + sep5);
23383
23546
  }
@@ -23601,7 +23764,7 @@ var WorktreeWebSocketHandler = class {
23601
23764
  }
23602
23765
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
23603
23766
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
23604
- const summary = await wt.diffSummary(resolve13(dir), base);
23767
+ const summary = await wt.diffSummary(resolve14(dir), base);
23605
23768
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
23606
23769
  }
23607
23770
  // ── internals ───────────────────────────────────────────────────────────
@@ -23766,6 +23929,11 @@ async function createAgentServices(input) {
23766
23929
  taskAware: config.Sage?.inject?.taskAware,
23767
23930
  minScore: config.Sage?.inject?.minScore,
23768
23931
  minImportance: config.Sage?.inject?.minImportance,
23932
+ // Forward the explicit relation floor so an operator-configured
23933
+ // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
23934
+ // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
23935
+ // is the CLI default but masks operator overrides.
23936
+ relationFloor: config.Sage?.inject?.relationFloor,
23769
23937
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
23770
23938
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
23771
23939
  triggers: config.Sage?.inject?.triggers,
@@ -23787,6 +23955,10 @@ async function createAgentServices(input) {
23787
23955
  maxMemories: config.Sage?.inject?.maxTurnMemories,
23788
23956
  maxChars: config.Sage?.inject?.maxCharsPerTurn,
23789
23957
  minScore: config.Sage?.inject?.minScore,
23958
+ // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
23959
+ // value drives both runtimes instead of silently falling back to the
23960
+ // 0.3 default. The undefined case keeps the middleware's own default.
23961
+ metadataWeight: config.Sage?.retrieval?.metadataWeight,
23790
23962
  getSessionId: getSageSessionId,
23791
23963
  tracker: sageInjectionTracker
23792
23964
  })
@@ -23811,25 +23983,29 @@ async function createAgentServices(input) {
23811
23983
  strategy: config.context?.strategy,
23812
23984
  preserveK: config.context?.preserveK ?? 10,
23813
23985
  eliseThreshold: config.context?.eliseThreshold ?? 2e3,
23986
+ // Match the CLI/TUI runtime: keep corrections, errors and decisions
23987
+ // verbatim while collapsing routine assistant chatter/tool protocol.
23988
+ // Without this WebUI's hybrid strategy builds an ever-growing lossless
23989
+ // digest and eventually relies on blunt emergency head/tail trimming.
23990
+ smart: true,
23814
23991
  summarizerModel: config.context?.summarizerModel,
23815
23992
  llmSelector: config.context?.llmSelector
23816
23993
  });
23817
23994
  const initialContextPolicy = resolveContextWindowPolicy2(config.context);
23818
23995
  let autoCompactor;
23819
23996
  if (config.context?.autoCompact !== false) {
23820
- let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23821
- if (!effectiveMaxContext) {
23822
- try {
23823
- const m = await resolveProviderModelMetadata(
23824
- modelsRegistry,
23825
- config.provider,
23826
- context.model,
23827
- config.providers?.[config.provider]
23828
- );
23829
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23830
- } catch {
23831
- }
23997
+ let effectiveMaxContext = 0;
23998
+ try {
23999
+ const m = await resolveProviderModelMetadata(
24000
+ modelsRegistry,
24001
+ config.provider,
24002
+ context.model,
24003
+ config.providers?.[config.provider]
24004
+ );
24005
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
24006
+ } catch {
23832
24007
  }
24008
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23833
24009
  if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23834
24010
  autoCompactor = new AutoCompactionMiddlewareCtor(
23835
24011
  compactor,
@@ -24451,16 +24627,24 @@ function createMessageDispatcher(opts) {
24451
24627
  getAgent: () => deps2.agent,
24452
24628
  getSessionId: () => state.getSession().id,
24453
24629
  runControl: {
24454
- begin: () => {
24630
+ begin: (_ws, sessionId) => {
24455
24631
  if (runLock.get()) return void 0;
24456
24632
  const controller = new AbortController();
24457
24633
  runLock.set(controller);
24634
+ runLock.setSession(sessionId);
24458
24635
  return controller;
24459
24636
  },
24460
- end: (_ws, controller) => {
24461
- if (runLock.get() === controller) runLock.set(null);
24637
+ end: (_ws, _sessionId, controller) => {
24638
+ if (runLock.get() === controller) {
24639
+ runLock.set(null);
24640
+ runLock.setSession(null);
24641
+ }
24462
24642
  },
24463
- abort: () => runLock.get()?.abort()
24643
+ abort: (_ws, sessionId) => {
24644
+ if (runLock.getSession() === sessionId || !runLock.getSession()) {
24645
+ runLock.get()?.abort();
24646
+ }
24647
+ }
24464
24648
  },
24465
24649
  pendingConfirms,
24466
24650
  send,
@@ -24482,10 +24666,20 @@ function createMessageDispatcher(opts) {
24482
24666
  const goalSnapshotRoutes = {
24483
24667
  getSnapshot: () => handleGoalGet(state.getProjectRoot(), (message) => broadcast(state.getClients(), message))
24484
24668
  };
24669
+ const kanbanSupervisor = createKanbanSupervisor({
24670
+ projectRoot: () => state.getProjectRoot(),
24671
+ broadcast: (message) => broadcast(state.getClients(), message),
24672
+ log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
24673
+ });
24674
+ if (opts.onDispose) {
24675
+ const dispose = () => kanbanSupervisor.dispose();
24676
+ opts.onDispose(dispose);
24677
+ }
24485
24678
  const kanbanContext = () => ({
24486
24679
  projectRoot: state.getProjectRoot(),
24487
24680
  context: deps2.context,
24488
- broadcast: (message) => broadcast(state.getClients(), message)
24681
+ broadcast: (message) => broadcast(state.getClients(), message),
24682
+ supervisor: kanbanSupervisor
24489
24683
  });
24490
24684
  const kanbanHostRoutes = {
24491
24685
  meta: async (ws) => {
@@ -25001,6 +25195,14 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
25001
25195
  transition = transition.then(async () => {
25002
25196
  if (stopped) return;
25003
25197
  if (pendingClaim?.sessionId === sessionId) {
25198
+ await pendingClaim.claim.activate({
25199
+ sessionId,
25200
+ ...target,
25201
+ clientType: "webui",
25202
+ pid: process.pid,
25203
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
25204
+ agents: statusTracker.getAgents()
25205
+ });
25004
25206
  pendingClaim = void 0;
25005
25207
  } else {
25006
25208
  await register(sessionId, true, target);
@@ -25024,14 +25226,35 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
25024
25226
  }
25025
25227
  if (sessionId === activeSessionId) return async () => {
25026
25228
  };
25027
- const previousSessionId = activeSessionId;
25028
- const previousTarget = activeTarget;
25029
25229
  const token = Symbol(sessionId);
25030
- await register(sessionId, true, target);
25031
- pendingClaim = { sessionId, previousSessionId, token };
25230
+ if ("reserveResume" in registry && typeof registry.reserveResume === "function") {
25231
+ const reservation = await registry.reserveResume({
25232
+ sessionId,
25233
+ projectSlug: target.projectSlug,
25234
+ projectRoot: target.projectRoot
25235
+ });
25236
+ pendingClaim = { sessionId, token, claim: reservation, target };
25237
+ } else {
25238
+ await register(sessionId, true, target);
25239
+ pendingClaim = {
25240
+ sessionId,
25241
+ token,
25242
+ target,
25243
+ claim: {
25244
+ reservation: {
25245
+ reservationId: "legacy",
25246
+ targetSessionId: sessionId,
25247
+ requesterInstanceId: "legacy",
25248
+ expiresAt: Number.MAX_SAFE_INTEGER
25249
+ },
25250
+ activate: async () => void 0,
25251
+ cancel: async () => register(activeSessionId, true, activeTarget)
25252
+ }
25253
+ };
25254
+ }
25032
25255
  return async () => {
25033
25256
  if (pendingClaim?.token !== token) return;
25034
- await register(previousSessionId, true, previousTarget);
25257
+ await pendingClaim.claim.cancel();
25035
25258
  pendingClaim = void 0;
25036
25259
  };
25037
25260
  };
@@ -25958,10 +26181,11 @@ function startHttpServer(opts) {
25958
26181
  return httpServer;
25959
26182
  }
25960
26183
  function registerShutdown(deps2) {
25961
- registerShutdownHandlers({
26184
+ return registerShutdownHandlers({
25962
26185
  flushSession: deps2.flushSession,
25963
26186
  clients: deps2.clients,
25964
26187
  servers: deps2.servers,
26188
+ onPreShutdown: deps2.onPreShutdown,
25965
26189
  onShutdown: deps2.onShutdown
25966
26190
  });
25967
26191
  }
@@ -26231,7 +26455,7 @@ async function startWebUI(opts = {}) {
26231
26455
  if (events.listenerCount("tool.confirm_needed") === 0) {
26232
26456
  throw new Error("No permission confirmation surface is connected");
26233
26457
  }
26234
- const decision = await new Promise((resolve16) => {
26458
+ const decision = await new Promise((resolve17) => {
26235
26459
  events.emit("tool.confirm_needed", {
26236
26460
  sessionId: context.session.id,
26237
26461
  tool: confirmTool,
@@ -26241,7 +26465,7 @@ async function startWebUI(opts = {}) {
26241
26465
  decisionSource: pending.decisionSource,
26242
26466
  riskTier: pending.riskTier,
26243
26467
  boundaryReason: pending.boundaryReason,
26244
- resolve: resolve16
26468
+ resolve: resolve17
26245
26469
  });
26246
26470
  });
26247
26471
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -26276,10 +26500,15 @@ async function startWebUI(opts = {}) {
26276
26500
  );
26277
26501
  }
26278
26502
  let _runLock = null;
26503
+ let _runLockSession = null;
26279
26504
  const runLockControl = {
26280
26505
  get: () => _runLock,
26281
26506
  set: (ctrl) => {
26282
26507
  _runLock = ctrl;
26508
+ },
26509
+ getSession: () => _runLockSession,
26510
+ setSession: (id) => {
26511
+ _runLockSession = id;
26283
26512
  }
26284
26513
  };
26285
26514
  const pendingConfirms = /* @__PURE__ */ new Map();
@@ -26404,6 +26633,7 @@ async function startWebUI(opts = {}) {
26404
26633
  if (ctrl) {
26405
26634
  ctrl.abort();
26406
26635
  runLockControl.set(null);
26636
+ runLockControl.setSession(null);
26407
26637
  }
26408
26638
  },
26409
26639
  isRunActive: () => runLockControl.get() !== null,
@@ -26553,6 +26783,7 @@ async function startWebUI(opts = {}) {
26553
26783
  })
26554
26784
  });
26555
26785
  const routes = buildRoutes(state, deps2, cb);
26786
+ let kanbanSupervisorDispose = null;
26556
26787
  const handleMessage = createMessageDispatcher({
26557
26788
  state,
26558
26789
  deps: deps2,
@@ -26560,7 +26791,10 @@ async function startWebUI(opts = {}) {
26560
26791
  promptsCtx,
26561
26792
  codebaseIndexing,
26562
26793
  runLock: runLockControl,
26563
- pendingConfirms
26794
+ pendingConfirms,
26795
+ onDispose: (dispose) => {
26796
+ kanbanSupervisorDispose = dispose;
26797
+ }
26564
26798
  });
26565
26799
  const mailbox = getSharedProjectMailbox5(
26566
26800
  resolveProjectDir4(context.projectRoot, wstackGlobalRoot4()),
@@ -26594,7 +26828,14 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26594
26828
  priority: "high",
26595
26829
  senderSessionId: session.id
26596
26830
  }).catch((err) => {
26597
- console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
26831
+ console.warn(
26832
+ JSON.stringify({
26833
+ level: "warn",
26834
+ event: "webui.security_rejection_mailbox_note_failed",
26835
+ message: String(err),
26836
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26837
+ })
26838
+ );
26598
26839
  });
26599
26840
  },
26600
26841
  goalHandler,
@@ -26608,7 +26849,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26608
26849
  });
26609
26850
  wssPrimary.on("connection", handleConnection);
26610
26851
  if (wssSecondary) wssSecondary.on("connection", handleConnection);
26611
- registerShutdown({
26852
+ let unregisterShutdown = () => {
26853
+ };
26854
+ unregisterShutdown = registerShutdown({
26612
26855
  flushSession: async () => {
26613
26856
  await session.append({
26614
26857
  type: "session_end",
@@ -26624,7 +26867,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26624
26867
  wssPrimary,
26625
26868
  ...wssSecondary ? [wssSecondary] : []
26626
26869
  ],
26870
+ onPreShutdown: () => {
26871
+ kanbanSupervisorDispose?.();
26872
+ kanbanSupervisorDispose = null;
26873
+ },
26627
26874
  onShutdown: async () => {
26875
+ unregisterShutdown();
26628
26876
  await todosCheckpoint.detach();
26629
26877
  await stopHeapWatchdog();
26630
26878
  credentialWatcherClose?.();
@@ -26883,6 +27131,7 @@ export {
26883
27131
  isPortFree,
26884
27132
  isRegisteredMessageType,
26885
27133
  isWildcardBind,
27134
+ joinSessionRegistryWithWebUIInstances,
26886
27135
  listInstances,
26887
27136
  loadManifest,
26888
27137
  loadSavedProviders,