@wrongstack/webui-server 0.303.0 → 0.305.1

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
@@ -28,7 +28,14 @@ function isRecord(value) {
28
28
  var AUTONOMY_VALUES = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
29
29
  var CONTEXT_STRATEGY_VALUES = /* @__PURE__ */ new Set(["hybrid", "intelligent", "selective"]);
30
30
  var CONTEXT_MODE_VALUES = /* @__PURE__ */ new Set(["balanced", "frugal", "deep"]);
31
- var TOKEN_SAVING_TIER_VALUES = /* @__PURE__ */ new Set(["off", "minimal", "light", "medium", "aggressive"]);
31
+ var TOKEN_SAVING_TIER_VALUES = /* @__PURE__ */ new Set([
32
+ "auto",
33
+ "off",
34
+ "minimal",
35
+ "light",
36
+ "medium",
37
+ "aggressive"
38
+ ]);
32
39
  var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
33
40
  var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
34
41
  var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
@@ -285,12 +292,8 @@ function validatePreferenceValue(key, value) {
285
292
  if (!Array.isArray(value)) return `prefs.update payload.${key} must be an array`;
286
293
  for (let i = 0; i < value.length; i++) {
287
294
  const item = value[i];
288
- if (!isRecord(item))
289
- return `prefs.update payload.${key}[${i}] must be an object`;
290
- const error2 = arrayValidator(
291
- item,
292
- `prefs.update payload.${key}[${i}]`
293
- );
295
+ if (!isRecord(item)) return `prefs.update payload.${key}[${i}] must be an object`;
296
+ const error2 = arrayValidator(item, `prefs.update payload.${key}[${i}]`);
294
297
  if (error2) return error2;
295
298
  }
296
299
  return null;
@@ -1099,11 +1102,11 @@ import { randomBytes } from "node:crypto";
1099
1102
  import { scrubErrorDetail } from "@wrongstack/core/security";
1100
1103
  import { WebSocket } from "ws";
1101
1104
  var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
1102
- function sendSerialized(ws, data) {
1105
+ function sendSerialized(ws, data, frameBytes) {
1103
1106
  if (ws.readyState !== WebSocket.OPEN) return false;
1104
1107
  const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
1105
- const frameBytes = Buffer.byteLength(data, "utf8");
1106
- if (buffered + frameBytes > WEBUI_WS_MAX_BUFFERED_BYTES) {
1108
+ const bytes = frameBytes ?? Buffer.byteLength(data, "utf8");
1109
+ if (buffered + bytes > WEBUI_WS_MAX_BUFFERED_BYTES) {
1107
1110
  try {
1108
1111
  ws.terminate();
1109
1112
  } catch {
@@ -1126,8 +1129,9 @@ function send(ws, msg) {
1126
1129
  }
1127
1130
  function broadcast(clients, msg) {
1128
1131
  const data = JSON.stringify(msg);
1132
+ const frameBytes = Buffer.byteLength(data, "utf8");
1129
1133
  for (const [ws] of clients) {
1130
- sendSerialized(ws, data);
1134
+ sendSerialized(ws, data, frameBytes);
1131
1135
  }
1132
1136
  }
1133
1137
  function sendResult2(ws, success, message) {
@@ -2202,6 +2206,7 @@ var CollaborationWebSocketHandler = class {
2202
2206
  this.broadcast(sessionId, this.stateMessage(sessionId));
2203
2207
  }
2204
2208
  }, 2e3);
2209
+ this.broadcastInterval.unref?.();
2205
2210
  }
2206
2211
  stopBroadcast() {
2207
2212
  if (this.broadcastInterval) {
@@ -7217,13 +7222,18 @@ var GoalWebSocketHandler = class {
7217
7222
  const { execFile: execFile2 } = await import("node:child_process");
7218
7223
  const result = await new Promise((resolve17) => {
7219
7224
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7220
- execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7221
- if (err && err.code === "ENOENT") {
7222
- resolve17("[verify] tsc not found \u2014 skipping");
7223
- return;
7225
+ execFile2(
7226
+ npxCommand,
7227
+ ["tsc", "--noEmit"],
7228
+ { cwd, timeout: 6e4 },
7229
+ (err, stdout, stderr) => {
7230
+ if (err && err.code === "ENOENT") {
7231
+ resolve17("[verify] tsc not found \u2014 skipping");
7232
+ return;
7233
+ }
7234
+ resolve17(stdout + stderr);
7224
7235
  }
7225
- resolve17(stdout + stderr);
7226
- });
7236
+ );
7227
7237
  });
7228
7238
  if (result.includes("[verify]") || result.trim().length === 0) {
7229
7239
  return { ok: true };
@@ -7525,6 +7535,7 @@ ${result_.finalText.slice(0, 2e3)}`
7525
7535
  if (progress) this.broadcast({ type: "goal.progress", payload: progress });
7526
7536
  this.broadcastState();
7527
7537
  }, 2e3);
7538
+ this.broadcastInterval.unref?.();
7528
7539
  }
7529
7540
  stopBroadcast() {
7530
7541
  if (this.broadcastInterval) {
@@ -7649,8 +7660,9 @@ ${result_.finalText.slice(0, 2e3)}`
7649
7660
  }
7650
7661
  broadcast(msg) {
7651
7662
  const data = JSON.stringify(msg);
7663
+ const frameBytes = Buffer.byteLength(data, "utf8");
7652
7664
  for (const client of this.clients) {
7653
- sendSerialized(client.ws, data);
7665
+ sendSerialized(client.ws, data, frameBytes);
7654
7666
  }
7655
7667
  }
7656
7668
  send(client, msg) {
@@ -9561,7 +9573,19 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
9561
9573
  if (!host) return false;
9562
9574
  const hostUrl = new URL(`${url.protocol}//${host}`);
9563
9575
  if (!isLoopbackHostname(hostUrl.hostname)) return false;
9564
- return effectivePort(url) === effectivePort(hostUrl);
9576
+ return normalizeHostname(url.hostname) === normalizeHostname(hostUrl.hostname) && effectivePort(url) === effectivePort(hostUrl);
9577
+ } catch {
9578
+ return false;
9579
+ }
9580
+ }
9581
+ function originMatchesHost(origin, hostHeader) {
9582
+ try {
9583
+ const originUrl = new URL(origin);
9584
+ if (originUrl.protocol !== "http:" && originUrl.protocol !== "https:") return false;
9585
+ const host = (hostHeader ?? "").trim();
9586
+ if (!host) return false;
9587
+ const requestUrl = new URL(`${originUrl.protocol}//${host}`);
9588
+ return normalizeHostname(originUrl.hostname) === normalizeHostname(requestUrl.hostname) && effectivePort(originUrl) === effectivePort(requestUrl);
9565
9589
  } catch {
9566
9590
  return false;
9567
9591
  }
@@ -9666,13 +9690,15 @@ function verifyClient(input) {
9666
9690
  try {
9667
9691
  const { hostname: originHostname } = new URL(origin);
9668
9692
  if (isLoopbackHostname(originHostname)) {
9669
- if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
9693
+ if (requireToken || !isLoopbackBind(wsHost)) {
9694
+ return cookieTokenOk && (originMatchesHost(origin, hostHeader) || Boolean(allowCrossPortLoopbackCookie));
9695
+ }
9670
9696
  if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
9671
9697
  return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
9672
9698
  }
9673
9699
  return true;
9674
9700
  }
9675
- return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9701
+ return cookieTokenOk && originMatchesHost(origin, hostHeader) || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
9676
9702
  } catch {
9677
9703
  return false;
9678
9704
  }
@@ -10637,7 +10663,8 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10637
10663
  switch (message.type) {
10638
10664
  case "diag.get": {
10639
10665
  if (!sessionAllowed(ctx, ws, message)) return true;
10640
- const tools = ctx.agent.tools.list();
10666
+ const registry = ctx.agent.tools;
10667
+ const tools = registry.listForProvider?.() ?? registry.list();
10641
10668
  ctx.send(ws, {
10642
10669
  type: "diag.get",
10643
10670
  payload: {
@@ -10720,6 +10747,7 @@ async function handleIntrospectionRoute(ctx, ws, message) {
10720
10747
  description: tool.description ?? "",
10721
10748
  params: schema.properties ? Object.keys(schema.properties) : [],
10722
10749
  disabled: registry.isDisabled?.(tool.name) ?? false,
10750
+ direct: registry.isExposedToProvider?.(tool.name) ?? true,
10723
10751
  mutating: !!tool.mutating,
10724
10752
  permission: tool.permission ?? "auto"
10725
10753
  };
@@ -11029,7 +11057,6 @@ async function handleKanbanHostRoute(ws, msg, handlers) {
11029
11057
  import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
11030
11058
  import {
11031
11059
  addCheckToTask,
11032
- addColumn,
11033
11060
  addGoalMetricToTask,
11034
11061
  addNoteToTask,
11035
11062
  addTask,
@@ -11044,7 +11071,10 @@ import {
11044
11071
  getKanbanOrchestrationSnapshot,
11045
11072
  getKanbanQueueHealth,
11046
11073
  getTaskChain,
11074
+ hasKanbanQueueAnomalies,
11075
+ kanbanQueueAnomalyCount,
11047
11076
  listBoards as listBoards2,
11077
+ listBoardHistory,
11048
11078
  listReadyTasks,
11049
11079
  mergeTasks,
11050
11080
  moveTask,
@@ -11053,7 +11083,6 @@ import {
11053
11083
  recoverStaleTaskAssignments,
11054
11084
  releaseTaskClaim,
11055
11085
  removeBoard,
11056
- removeColumn,
11057
11086
  setTaskChain,
11058
11087
  splitTask,
11059
11088
  syncBoardFromTaskGraph,
@@ -11065,14 +11094,16 @@ import {
11065
11094
  updateTask as updateTask2
11066
11095
  } from "@wrongstack/kanban";
11067
11096
 
11068
- // src/server/kanban-decomposition-routes.ts
11097
+ // src/server/kanban-contract-routes.ts
11069
11098
  import {
11070
- listBoards,
11071
- resolveDecompositionProposal,
11072
- updateTask,
11073
- verifyTaskCompletion
11099
+ addContractEdge,
11100
+ configureContractGraph,
11101
+ evaluateTaskContractGraph,
11102
+ getContractGraph,
11103
+ removeContractEdge,
11104
+ removeContractNode,
11105
+ upsertContractNode
11074
11106
  } from "@wrongstack/kanban";
11075
- import { recordKanbanVerificationEvidence as recordKanbanVerificationEvidence2 } from "@wrongstack/tools";
11076
11107
 
11077
11108
  // src/server/kanban-route-helpers.ts
11078
11109
  import { touchKanbanPresence } from "@wrongstack/kanban";
@@ -11124,7 +11155,150 @@ function findTask2(tasks, taskId) {
11124
11155
  return tasks.find((task) => task.id === taskId || task.id.startsWith(taskId));
11125
11156
  }
11126
11157
 
11158
+ // src/server/kanban-contract-routes.ts
11159
+ async function handleKanbanContractRoute(ws, type, payload, ctx) {
11160
+ switch (type) {
11161
+ case "kanban.contract.get":
11162
+ await handleGet(ws, type, payload, ctx);
11163
+ return true;
11164
+ case "kanban.contract.configure":
11165
+ await handleConfigure(ws, type, payload, ctx);
11166
+ return true;
11167
+ case "kanban.contract.node.upsert":
11168
+ await handleNodeUpsert(ws, type, payload, ctx);
11169
+ return true;
11170
+ case "kanban.contract.node.remove":
11171
+ await handleNodeRemove(ws, type, payload, ctx);
11172
+ return true;
11173
+ case "kanban.contract.edge.add":
11174
+ await handleEdgeAdd(ws, type, payload, ctx);
11175
+ return true;
11176
+ case "kanban.contract.edge.remove":
11177
+ await handleEdgeRemove(ws, type, payload, ctx);
11178
+ return true;
11179
+ default:
11180
+ return false;
11181
+ }
11182
+ }
11183
+ var str = (payload, key) => typeof payload?.[key] === "string" ? payload[key] : void 0;
11184
+ async function publishBoard(ctx, board) {
11185
+ await publishKanbanBoard((message) => ctx.broadcast?.(message), board);
11186
+ }
11187
+ async function handleGet(ws, type, payload, ctx) {
11188
+ const boardId = str(payload, "boardId");
11189
+ if (!boardId) return fail(ws, type, "boardId required");
11190
+ const found = await getContractGraph(ctx.projectRoot, boardId);
11191
+ if (!found) return fail(ws, type, "Board not found");
11192
+ const taskId = str(payload, "taskId");
11193
+ const evaluated = taskId ? await evaluateTaskContractGraph(ctx.projectRoot, boardId, taskId) : null;
11194
+ if (taskId && !evaluated) return fail(ws, type, "Task not found on this board");
11195
+ ok(ws, type, {
11196
+ boardId,
11197
+ graph: found.graph,
11198
+ ...evaluated ? { evaluation: evaluated.evaluation } : {}
11199
+ });
11200
+ }
11201
+ async function handleConfigure(ws, type, payload, ctx) {
11202
+ const boardId = str(payload, "boardId");
11203
+ if (!boardId) return fail(ws, type, "boardId required");
11204
+ const enforcement = str(payload, "enforcement") ?? "advisory";
11205
+ const board = await configureContractGraph(ctx.projectRoot, boardId, enforcement);
11206
+ if (!board) return fail(ws, type, "Board not found");
11207
+ await publishBoard(ctx, board);
11208
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11209
+ }
11210
+ async function handleNodeUpsert(ws, type, payload, ctx) {
11211
+ const boardId = str(payload, "boardId");
11212
+ const taskId = str(payload, "taskId");
11213
+ const kind = str(payload, "kind");
11214
+ const title = str(payload, "title");
11215
+ if (!boardId || !taskId || !kind || !title) {
11216
+ return fail(ws, type, "boardId, taskId, kind, and title required");
11217
+ }
11218
+ const state = str(payload, "state");
11219
+ const waiverActor = str(payload, "waiverActor");
11220
+ const waiverReason = str(payload, "waiverReason");
11221
+ if (state === "waived" && (!waiverActor?.trim() || !waiverReason?.trim())) {
11222
+ return fail(ws, type, "A waived contract node requires waiverActor and waiverReason");
11223
+ }
11224
+ try {
11225
+ const result = await upsertContractNode(ctx.projectRoot, boardId, {
11226
+ taskId,
11227
+ kind,
11228
+ title,
11229
+ ...str(payload, "nodeId") !== void 0 ? { id: str(payload, "nodeId") } : {},
11230
+ ...str(payload, "description") !== void 0 ? { description: str(payload, "description") } : {},
11231
+ ...state !== void 0 ? { state } : {},
11232
+ ...str(payload, "enforcement") !== void 0 ? { enforcement: str(payload, "enforcement") } : {},
11233
+ ...str(payload, "checkId") !== void 0 ? { checkId: str(payload, "checkId") } : {},
11234
+ ...str(payload, "metricId") !== void 0 ? { metricId: str(payload, "metricId") } : {},
11235
+ ...state === "waived" ? {
11236
+ waiver: {
11237
+ actor: waiverActor,
11238
+ reason: waiverReason,
11239
+ at: (/* @__PURE__ */ new Date()).toISOString()
11240
+ }
11241
+ } : {},
11242
+ ...str(payload, "createdBy") !== void 0 ? { createdBy: str(payload, "createdBy") } : { createdBy: "webui" }
11243
+ });
11244
+ if (!result) return fail(ws, type, "Board or task not found");
11245
+ await publishBoard(ctx, result.board);
11246
+ ok(ws, type, { boardId, node: result.node, graph: result.board.contractGraph ?? null });
11247
+ } catch (err) {
11248
+ fail(ws, type, err instanceof Error ? err.message : String(err));
11249
+ }
11250
+ }
11251
+ async function handleNodeRemove(ws, type, payload, ctx) {
11252
+ const boardId = str(payload, "boardId");
11253
+ const nodeId = str(payload, "nodeId");
11254
+ if (!boardId || !nodeId) return fail(ws, type, "boardId and nodeId required");
11255
+ const board = await removeContractNode(ctx.projectRoot, boardId, nodeId);
11256
+ if (!board) return fail(ws, type, "Contract node not found");
11257
+ await publishBoard(ctx, board);
11258
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11259
+ }
11260
+ async function handleEdgeAdd(ws, type, payload, ctx) {
11261
+ const boardId = str(payload, "boardId");
11262
+ const from = str(payload, "from");
11263
+ const to = str(payload, "to");
11264
+ const edgeType = str(payload, "edgeType");
11265
+ if (!boardId || !from || !to || !edgeType) {
11266
+ return fail(ws, type, "boardId, from, to, and edgeType required");
11267
+ }
11268
+ try {
11269
+ const result = await addContractEdge(ctx.projectRoot, boardId, {
11270
+ from,
11271
+ to,
11272
+ type: edgeType,
11273
+ ...str(payload, "enforcement") !== void 0 ? { enforcement: str(payload, "enforcement") } : {},
11274
+ ...str(payload, "rationale") !== void 0 ? { rationale: str(payload, "rationale") } : {},
11275
+ createdBy: str(payload, "createdBy") ?? "webui"
11276
+ });
11277
+ if (!result) return fail(ws, type, "Board not found");
11278
+ await publishBoard(ctx, result.board);
11279
+ ok(ws, type, { boardId, edge: result.edge, graph: result.board.contractGraph ?? null });
11280
+ } catch (err) {
11281
+ fail(ws, type, err instanceof Error ? err.message : String(err));
11282
+ }
11283
+ }
11284
+ async function handleEdgeRemove(ws, type, payload, ctx) {
11285
+ const boardId = str(payload, "boardId");
11286
+ const edgeId = str(payload, "edgeId");
11287
+ if (!boardId || !edgeId) return fail(ws, type, "boardId and edgeId required");
11288
+ const board = await removeContractEdge(ctx.projectRoot, boardId, edgeId);
11289
+ if (!board) return fail(ws, type, "Contract edge not found");
11290
+ await publishBoard(ctx, board);
11291
+ ok(ws, type, { boardId, graph: board.contractGraph ?? null });
11292
+ }
11293
+
11127
11294
  // src/server/kanban-decomposition-routes.ts
11295
+ import {
11296
+ listBoards,
11297
+ resolveDecompositionProposal,
11298
+ updateTask,
11299
+ verifyTaskCompletion
11300
+ } from "@wrongstack/kanban";
11301
+ import { recordKanbanVerificationEvidence as recordKanbanVerificationEvidence2 } from "@wrongstack/tools";
11128
11302
  async function handleKanbanDecompositionRoute(ws, type, payload, ctx) {
11129
11303
  switch (type) {
11130
11304
  case "kanban.decomposition.approve":
@@ -11373,8 +11547,12 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
11373
11547
  // src/server/kanban-route-protocol.ts
11374
11548
  var KANBAN_CLIENT_MESSAGE_TYPES = [
11375
11549
  "kanban.capabilities",
11376
- "kanban.column.add",
11377
- "kanban.column.remove",
11550
+ "kanban.contract.configure",
11551
+ "kanban.contract.edge.add",
11552
+ "kanban.contract.edge.remove",
11553
+ "kanban.contract.get",
11554
+ "kanban.contract.node.remove",
11555
+ "kanban.contract.node.upsert",
11378
11556
  "kanban.create",
11379
11557
  "kanban.decomposition.approve",
11380
11558
  "kanban.decomposition.reject",
@@ -11425,6 +11603,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
11425
11603
  const type = msg.type;
11426
11604
  try {
11427
11605
  if (await handleKanbanDecompositionRoute(ws, type, payload, ctx)) return true;
11606
+ if (await handleKanbanContractRoute(ws, type, payload, ctx)) return true;
11428
11607
  if (await handleKanbanTaskRoute(ws, type, payload, ctx)) return true;
11429
11608
  switch (type) {
11430
11609
  case "kanban.list": {
@@ -11463,7 +11642,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
11463
11642
  fail(ws, type, "boardId required");
11464
11643
  return true;
11465
11644
  }
11466
- ok(ws, type, await getKanbanQueueHealth(ctx.projectRoot, { boardId: hBoardId }));
11645
+ ok(
11646
+ ws,
11647
+ type,
11648
+ await getKanbanQueueHealth(ctx.projectRoot, {
11649
+ boardId: hBoardId,
11650
+ includeClassifications: false
11651
+ })
11652
+ );
11467
11653
  return true;
11468
11654
  }
11469
11655
  case "kanban.supervisor.status":
@@ -11493,16 +11679,16 @@ async function handleKanbanRoute(ws, msg, ctx) {
11493
11679
  reason: "On-demand standalone Kanban supervisor audit."
11494
11680
  }) : null;
11495
11681
  if (recovered) health = await getKanbanQueueHealth(ctx.projectRoot, { boardId });
11496
- const anomalyCount = health.staleAssignments.count + health.dependencyBlocked.count + health.failedRetryable.count + health.counts.failed + health.counts.blocked;
11682
+ const anomalyCount = kanbanQueueAnomalyCount(health);
11497
11683
  ok(ws, type, {
11498
11684
  boardId,
11499
- status: board.supervisor?.enabled === false ? "disabled" : anomalyCount ? "attention" : "healthy",
11685
+ status: board.supervisor?.enabled === false ? "disabled" : hasKanbanQueueAnomalies(health) ? "attention" : "healthy",
11500
11686
  mode: board.supervisor?.mode ?? "deterministic",
11501
11687
  lastAuditAt: (/* @__PURE__ */ new Date()).toISOString(),
11502
11688
  reconciledTaskIds: reconciled?.tasks.map((task) => task.id) ?? [],
11503
11689
  staleRecoveredTaskIds: recovered?.tasks.map((task) => task.id) ?? [],
11504
11690
  anomalyCount,
11505
- summary: `${health.counts.running} running \xB7 ${health.counts.ready} ready \xB7 ${health.counts.review} review \xB7 ${health.counts.blocked} blocked \xB7 ${health.counts.failed} failed`
11691
+ summary: `${health.counts.running} running \xB7 ${health.counts.startable} ready \xB7 ${health.counts.review} review \xB7 ${health.counts.blocked} blocked \xB7 ${health.counts.failed} failed`
11506
11692
  });
11507
11693
  return true;
11508
11694
  }
@@ -11519,7 +11705,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11519
11705
  title,
11520
11706
  ...payload?.description ? { description: payload.description } : {},
11521
11707
  ...payload?.tags ? { tags: payload.tags } : {},
11522
- ...payload?.columns ? { columns: payload.columns } : {},
11523
11708
  ...has(payload, "lifecycle") ? { lifecycle: payload?.lifecycle } : {},
11524
11709
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
11525
11710
  })
@@ -11536,7 +11721,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
11536
11721
  ...payload?.title ? { title: payload.title } : {},
11537
11722
  ...payload?.description ? { description: payload.description } : {},
11538
11723
  ...payload?.tags ? { tags: payload.tags } : {},
11539
- ...payload?.columns ? { columns: payload.columns } : {},
11540
11724
  ...has(payload, "lifecycle") ? {
11541
11725
  lifecycle: payload?.lifecycle ?? null
11542
11726
  } : {},
@@ -11583,6 +11767,12 @@ async function handleKanbanRoute(ws, msg, ctx) {
11583
11767
  });
11584
11768
  return true;
11585
11769
  }
11770
+ case "kanban.board.history": {
11771
+ const boardId = payload?.boardId;
11772
+ const history = await listBoardHistory(ctx.projectRoot, boardId);
11773
+ ok(ws, type, history);
11774
+ return true;
11775
+ }
11586
11776
  case "kanban.generate": {
11587
11777
  const description = payload?.description;
11588
11778
  if (!description) {
@@ -12044,8 +12234,14 @@ async function handleKanbanRoute(ws, msg, ctx) {
12044
12234
  taskId,
12045
12235
  {
12046
12236
  description,
12237
+ // The old cast listed `manual | auto | agent | test | review` —
12238
+ // three of which have no verifier plugin, while the six that do
12239
+ // (command, file_exists, file_matches, git_diff, metric, test)
12240
+ // were unreachable. `notes` carries the executable body every
12241
+ // deterministic plugin reads.
12047
12242
  type: payload?.checkType ?? "manual",
12048
- status: payload?.status ?? "pending"
12243
+ status: payload?.status ?? "pending",
12244
+ ...typeof payload?.notes === "string" ? { notes: payload.notes } : {}
12049
12245
  },
12050
12246
  activityContext(
12051
12247
  ctx,
@@ -12110,30 +12306,6 @@ async function handleKanbanRoute(ws, msg, ctx) {
12110
12306
  case "kanban.capabilities":
12111
12307
  ok(ws, type, { dispatchSupported: Boolean(ctx.dispatchTask) });
12112
12308
  return true;
12113
- case "kanban.column.add": {
12114
- const boardId = payload?.boardId;
12115
- const title = payload?.title;
12116
- if (!boardId || !title) {
12117
- fail(ws, type, "boardId and title required");
12118
- return true;
12119
- }
12120
- const result = await addColumn(ctx.projectRoot, boardId, { title });
12121
- result ? ok(ws, type, result.board.columns) : fail(ws, type, `Board not found: ${boardId}`);
12122
- return true;
12123
- }
12124
- case "kanban.column.remove": {
12125
- const boardId = payload?.boardId;
12126
- const columnId = payload?.columnId;
12127
- if (!boardId || !columnId) {
12128
- fail(ws, type, "boardId and columnId required");
12129
- return true;
12130
- }
12131
- const board = await removeColumn(ctx.projectRoot, boardId, columnId, {
12132
- moveTasksToColumnId: payload?.moveTasksToColumnId
12133
- });
12134
- board ? ok(ws, type, { removed: true, boardId: board.id, columnId, board }) : fail(ws, type, `Column not found: ${columnId}`);
12135
- return true;
12136
- }
12137
12309
  default:
12138
12310
  fail(ws, type, `Unknown kanban message type: ${type}`);
12139
12311
  return true;
@@ -13403,6 +13575,34 @@ async function handleSageRecover(ws, msg, memoryStore) {
13403
13575
  send(ws, { type: "memory.sage.recover", payload: { error: errMessage(err) } });
13404
13576
  }
13405
13577
  }
13578
+ async function handleSageListCandidates(ws, msg, memoryStore) {
13579
+ const Sage = getSageSurface(memoryStore);
13580
+ if (!Sage) {
13581
+ send(ws, {
13582
+ type: "memory.sage.listCandidates",
13583
+ payload: { error: requiresSage("memory.sage.listCandidates") }
13584
+ });
13585
+ return;
13586
+ }
13587
+ try {
13588
+ const payload = msg.payload ?? {};
13589
+ const includeResolved = payload["includeResolved"] === true;
13590
+ if (typeof Sage.listCandidates !== "function") {
13591
+ send(ws, {
13592
+ type: "memory.sage.listCandidates",
13593
+ payload: { error: "listCandidates is not available on this SAGE surface" }
13594
+ });
13595
+ return;
13596
+ }
13597
+ const candidates = await Sage.listCandidates(includeResolved);
13598
+ send(ws, { type: "memory.sage.listCandidates", payload: { candidates } });
13599
+ } catch (err) {
13600
+ send(ws, {
13601
+ type: "memory.sage.listCandidates",
13602
+ payload: { error: errMessage(err) }
13603
+ });
13604
+ }
13605
+ }
13406
13606
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
13407
13607
  const Sage = getSageSurface(memoryStore);
13408
13608
  if (!Sage) {
@@ -13581,6 +13781,9 @@ async function handleMemoryRoute(ctx, ws, message) {
13581
13781
  case "memory.sage.recover":
13582
13782
  await handleSageRecover(ws, message, store);
13583
13783
  return true;
13784
+ case "memory.sage.listCandidates":
13785
+ await handleSageListCandidates(ws, message, store);
13786
+ return true;
13584
13787
  case "memory.sage.candidateResolve":
13585
13788
  await handleSageCandidateResolve(ws, message, store);
13586
13789
  return true;
@@ -15147,6 +15350,7 @@ import {
15147
15350
  finalizeTaskCompletion,
15148
15351
  getBoard as getBoard3,
15149
15352
  getKanbanQueueHealth as getKanbanQueueHealth2,
15353
+ kanbanQueueAnomalyCount as kanbanQueueAnomalyCount2,
15150
15354
  listBoards as listBoards4,
15151
15355
  reconcileKanbanBoard as reconcileKanbanBoard2,
15152
15356
  recoverStaleTaskAssignments as recoverStaleTaskAssignments2,
@@ -15229,7 +15433,7 @@ function createKanbanSupervisor(deps2) {
15229
15433
  }) : null;
15230
15434
  if (recovered)
15231
15435
  health = await getKanbanQueueHealth2(resolveProjectRoot(deps2), { boardId: board.id });
15232
- const anomalyCount = countAnomalies(health);
15436
+ const anomalyCount = kanbanQueueAnomalyCount2(health);
15233
15437
  const snapshot = {
15234
15438
  boardId: board.id,
15235
15439
  status: anomalyCount > 0 ? "attention" : "healthy",
@@ -15428,13 +15632,10 @@ function dispatchRoute(routing) {
15428
15632
  ...routing.fallbackModels?.length ? { fallbackModels: routing.fallbackModels } : {}
15429
15633
  };
15430
15634
  }
15431
- function countAnomalies(health) {
15432
- return health.staleAssignments.count + health.heartbeatDue.count + health.counts.failed + health.counts.blocked;
15433
- }
15434
15635
  function healthSummary(health) {
15435
15636
  return [
15436
15637
  `${health.counts.running} running`,
15437
- `${health.counts.ready} ready`,
15638
+ `${health.counts.startable} ready`,
15438
15639
  `${health.counts.review} review`,
15439
15640
  `${health.counts.blocked} blocked`,
15440
15641
  `${health.counts.failed} failed`,
@@ -16196,7 +16397,7 @@ import { makeProviderFromConfig } from "@wrongstack/providers";
16196
16397
  import * as fs14 from "node:fs/promises";
16197
16398
  import * as path19 from "node:path";
16198
16399
  import { DefaultSessionStore } from "@wrongstack/core/storage";
16199
- import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
16400
+ import { activateProjectStateGuard, resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
16200
16401
  function createProjectHandlers(ctx) {
16201
16402
  const sendTo = (ws, message) => {
16202
16403
  if (ctx.sendMessage) ctx.sendMessage(ws, message);
@@ -16361,6 +16562,7 @@ function createProjectHandlers(ctx) {
16361
16562
  try {
16362
16563
  await ctx.onSessionSwapped?.(next.id, identityTarget);
16363
16564
  await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
16565
+ await activateProjectStateGuard(resolved);
16364
16566
  } catch (err) {
16365
16567
  try {
16366
16568
  await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
@@ -17378,6 +17580,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
17378
17580
  "memory.sage.get",
17379
17581
  "memory.sage.graph",
17380
17582
  "memory.sage.list",
17583
+ "memory.sage.listCandidates",
17381
17584
  "memory.sage.listPage",
17382
17585
  "memory.sage.recover",
17383
17586
  "memory.sage.remember",
@@ -17657,6 +17860,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
17657
17860
  "memory.sage.get",
17658
17861
  "memory.sage.graph",
17659
17862
  "memory.sage.list",
17863
+ "memory.sage.listCandidates",
17660
17864
  "memory.sage.listPage",
17661
17865
  "memory.sage.recover",
17662
17866
  "memory.sage.remember",
@@ -18508,6 +18712,7 @@ function createSessionHandlers(ctx) {
18508
18712
  ctx.abortActiveRun?.(current2.id);
18509
18713
  } catch {
18510
18714
  }
18715
+ await ctx.context.flushConversationJournal?.().catch(() => void 0);
18511
18716
  await finalizeSession(current2);
18512
18717
  }
18513
18718
  ctx.setSession(next);
@@ -19206,6 +19411,7 @@ import {
19206
19411
  clearProjectAgentConsolidated,
19207
19412
  clearProjectSkillAugmentation,
19208
19413
  createProjectAgent,
19414
+ DEFAULT_EAGER_SKILL_LIMIT,
19209
19415
  detectLearnedConflicts,
19210
19416
  evaluateAutoOptimize,
19211
19417
  FLEET_ROSTER,
@@ -19223,12 +19429,15 @@ import {
19223
19429
  loadProjectSkillAugmentation,
19224
19430
  loadSkillAffinity,
19225
19431
  optimizeProjectAgentLearning,
19432
+ rankRoleSkills,
19433
+ readQuarantinedDirectives,
19226
19434
  readRawLearnedEntries,
19227
19435
  resetProjectAgentIdentity,
19228
19436
  resolveAutoOptimizePolicy,
19229
19437
  resolveRoleSkillCandidates,
19230
19438
  saveProjectAgentConsolidated,
19231
19439
  saveProjectSkillAugmentation,
19440
+ scoreSkillAffinity,
19232
19441
  setSkillPinned,
19233
19442
  slugifyProjectAgentRole,
19234
19443
  updateProjectAgentConfig,
@@ -19535,9 +19744,7 @@ ${String(p.content ?? "")}`;
19535
19744
  // eligible right now, and why not when it does not. Surfacing the reason
19536
19745
  // is what keeps "nothing happened" from looking like a broken feature.
19537
19746
  case "agent-roster.auto-optimize-status": {
19538
- const policy = resolveAutoOptimizePolicy(
19539
- this.getAutoOptimizeSettings?.() ?? void 0
19540
- );
19747
+ const policy = resolveAutoOptimizePolicy(this.getAutoOptimizeSettings?.() ?? void 0);
19541
19748
  const roles = role ? [role] : listProjectAgentRoles(projectRoot);
19542
19749
  return {
19543
19750
  type,
@@ -19560,18 +19767,30 @@ ${String(p.content ?? "")}`;
19560
19767
  const candidates = resolveRoleSkillCandidates(role, projectRoot);
19561
19768
  const developed = listProjectSkillAugmentations(role, projectRoot);
19562
19769
  const affinity = loadSkillAffinity(role, projectRoot);
19770
+ const eager = new Set(rankRoleSkills(role, candidates, projectRoot));
19563
19771
  return {
19564
19772
  type,
19565
19773
  payload: {
19566
19774
  role,
19775
+ eagerLimit: DEFAULT_EAGER_SKILL_LIMIT,
19567
19776
  skills: candidates.map((skill) => ({
19568
19777
  skill,
19569
19778
  developed: developed.includes(skill),
19570
- affinity: affinity.entries[skill] ?? null
19779
+ affinity: affinity.entries[skill] ?? null,
19780
+ score: scoreSkillAffinity(affinity.entries[skill]) + (developed.includes(skill) ? 1 : 0),
19781
+ eager: eager.has(skill)
19571
19782
  }))
19572
19783
  }
19573
19784
  };
19574
19785
  }
19786
+ // ── Directives the loop stopped believing ─────────────────────────────
19787
+ case "agent-roster.quarantine": {
19788
+ if (!role) return { type, payload: { error: "role required" } };
19789
+ return {
19790
+ type,
19791
+ payload: { role, retired: readQuarantinedDirectives(role, projectRoot) }
19792
+ };
19793
+ }
19575
19794
  case "agent-roster.read-skill": {
19576
19795
  const skill = typeof p.skill === "string" ? p.skill : "";
19577
19796
  if (!role || !skill) return { type, payload: { error: "role and skill required" } };
@@ -21977,6 +22196,7 @@ function registerSetupEventsStatusWatcher(deps2) {
21977
22196
  const logWatcherMetricsEnabled = shouldLogWatcherStats();
21978
22197
  const logWatcherMetrics = () => logFileWatcherMetrics(watcherMetrics);
21979
22198
  const metricsInterval = logWatcherMetricsEnabled ? setInterval(logWatcherMetrics, 6e4) : void 0;
22199
+ metricsInterval?.unref?.();
21980
22200
  const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
21981
22201
  broadcast2(clients, { type: "client.status_update", payload: statusData });
21982
22202
  if (watcherMetrics) {
@@ -23196,14 +23416,7 @@ import {
23196
23416
  toErrorMessage as toErrorMessage10
23197
23417
  } from "@wrongstack/core/utils";
23198
23418
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
23199
- import {
23200
- createSageContextMonitorMiddleware,
23201
- createSageToolCallMiddleware,
23202
- createSageTurnMiddleware,
23203
- getSageRetrieval,
23204
- getSageService,
23205
- InjectionTracker
23206
- } from "@wrongstack/sage";
23419
+ import { getSageService, setupSage } from "@wrongstack/sage";
23207
23420
 
23208
23421
  // src/server/discover-mailbox-bridge.ts
23209
23422
  import { spawn as spawn4 } from "node:child_process";
@@ -24006,6 +24219,7 @@ var WorktreeWebSocketHandler = class {
24006
24219
  this.broadcast(this.stateMessage());
24007
24220
  if (this.broadcastInterval) return;
24008
24221
  this.broadcastInterval = setInterval(() => this.broadcast(this.stateMessage()), 2e3);
24222
+ this.broadcastInterval.unref?.();
24009
24223
  }
24010
24224
  stopBroadcast() {
24011
24225
  this.broadcast(this.stateMessage());
@@ -24057,62 +24271,15 @@ async function createAgentServices(input) {
24057
24271
  const collabPause = collabPauseMiddleware(collabBus, { logger });
24058
24272
  pipelines.toolCall.prepend(collabPause);
24059
24273
  installDesignStudioMiddleware({ pipelines, ctx: context });
24060
- const memoryRetrieval = getSageRetrieval(memoryStore);
24061
- if (config.features.memory !== false && config.Sage?.enabled !== false && memoryRetrieval) {
24062
- const sageInjectionTracker = new InjectionTracker();
24063
- const getSageSessionId = () => input.sessionGetter().id;
24064
- if (config.Sage?.inject?.toolResults !== false) {
24065
- pipelines.toolCall.use(
24066
- createSageToolCallMiddleware({
24067
- memory: memoryRetrieval,
24068
- maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
24069
- maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
24070
- taskAware: config.Sage?.inject?.taskAware,
24071
- minScore: config.Sage?.inject?.minScore,
24072
- minImportance: config.Sage?.inject?.minImportance,
24073
- // Forward the explicit relation floor so an operator-configured
24074
- // `Sage.inject.relationFloor` is honored in WebUI sessions. Without
24075
- // this we silently fall back to MIN_RELATION_STRENGTH (0.85), which
24076
- // is the CLI default but masks operator overrides.
24077
- relationFloor: config.Sage?.inject?.relationFloor,
24078
- repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
24079
- verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
24080
- triggers: config.Sage?.inject?.triggers,
24081
- // Resolve the live session so retrieval and cooldown are session-scoped
24082
- // (matching the CLI wiring in wiring/sage.ts). Without this, the
24083
- // middleware falls back to ctx.session.id for cooldown but passes
24084
- // undefined to retrieval, causing owned session-scoped memories to be
24085
- // silently excluded from tool-call injection.
24086
- getSessionId: getSageSessionId,
24087
- tracker: sageInjectionTracker,
24088
- events
24089
- })
24090
- );
24091
- }
24092
- if (config.Sage?.inject?.turnContext === true) {
24093
- pipelines.request.use(
24094
- createSageTurnMiddleware({
24095
- memory: memoryRetrieval,
24096
- maxMemories: config.Sage?.inject?.maxTurnMemories,
24097
- maxChars: config.Sage?.inject?.maxCharsPerTurn,
24098
- minScore: config.Sage?.inject?.minScore,
24099
- // CLI parity: honor `Sage.retrieval.metadataWeight` so the same config
24100
- // value drives both runtimes instead of silently falling back to the
24101
- // 0.3 default. The undefined case keeps the middleware's own default.
24102
- metadataWeight: config.Sage?.retrieval?.metadataWeight,
24103
- getSessionId: getSageSessionId,
24104
- tracker: sageInjectionTracker
24105
- })
24106
- );
24107
- }
24108
- pipelines.request.use(
24109
- createSageContextMonitorMiddleware({
24110
- tracker: sageInjectionTracker,
24111
- events,
24112
- getSessionId: getSageSessionId
24113
- })
24114
- );
24115
- }
24274
+ const runSageSessionHygiene = setupSage({
24275
+ config,
24276
+ pipelines,
24277
+ memoryStore,
24278
+ logger,
24279
+ events,
24280
+ getSessionId: () => input.sessionGetter().id,
24281
+ projectRoot
24282
+ });
24116
24283
  const codebaseIndexing = setupWebUICodebaseIndexing({
24117
24284
  config,
24118
24285
  context,
@@ -24224,7 +24391,12 @@ async function createAgentServices(input) {
24224
24391
  confirmAwaiter: void 0,
24225
24392
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
24226
24393
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
24227
- tracer: void 0
24394
+ tracer: void 0,
24395
+ // Off unless the operator opts in. The WebUI drives the same agent as the
24396
+ // CLI, so it must resolve this identically — a surface-dependent gate
24397
+ // would mean the same repo governs under `wstack` and not in the browser.
24398
+ // See packages/cli/src/wiring/pipeline.ts.
24399
+ requireKanbanGovernance: config.tools?.kanbanGovernance ?? DEFAULT_TOOLS_CONFIG.kanbanGovernance
24228
24400
  });
24229
24401
  input.installToolBoundary?.(pipelines);
24230
24402
  const webuiLogger = container.resolve(TOKENS2.Logger);
@@ -24244,6 +24416,7 @@ async function createAgentServices(input) {
24244
24416
  providers: providerRegistry,
24245
24417
  events,
24246
24418
  pipelines,
24419
+ refreshSystemPrompt: true,
24247
24420
  context,
24248
24421
  maxIterations: config.tools?.maxIterations ?? DEFAULT_TOOLS_CONFIG.maxIterations,
24249
24422
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
@@ -24511,6 +24684,7 @@ async function createAgentServices(input) {
24511
24684
  terminalHandler,
24512
24685
  collabHandler,
24513
24686
  disposeRealtimeHandlers,
24687
+ runSageSessionHygiene,
24514
24688
  updateAutoCompactionMaxContext
24515
24689
  };
24516
24690
  }
@@ -24990,15 +25164,9 @@ import {
24990
25164
  makeMailInboxTool,
24991
25165
  makeMailSendTool
24992
25166
  } from "@wrongstack/core/coordination";
24993
- import {
24994
- DefaultPromptLoader,
24995
- DefaultSkillLoader
24996
- } from "@wrongstack/core/execution";
24997
- import {
24998
- EventBus,
24999
- TOKENS as TOKENS3
25000
- } from "@wrongstack/core/kernel";
25167
+ import { DefaultPromptLoader, DefaultSkillLoader } from "@wrongstack/core/execution";
25001
25168
  import { DefaultTokenCounter } from "@wrongstack/core/infrastructure";
25169
+ import { EventBus, TOKENS as TOKENS3 } from "@wrongstack/core/kernel";
25002
25170
  import { DefaultModelsRegistry, DefaultModeStore } from "@wrongstack/core/models";
25003
25171
  import { ProviderRegistry, ToolRegistry } from "@wrongstack/core/registry";
25004
25172
  import { SkillInstaller } from "@wrongstack/core/skills";
@@ -25656,6 +25824,7 @@ async function createPreContextServices(input) {
25656
25824
  modeId,
25657
25825
  modePrompt,
25658
25826
  modelCapabilities: () => modelCapabilitiesRef.current,
25827
+ tokenSavingMode: config.features.tokenSavingMode,
25659
25828
  instructionPaths: {
25660
25829
  globalDir: wpaths.globalInstructions,
25661
25830
  projectDir: wpaths.inProjectInstructions,
@@ -25676,7 +25845,8 @@ async function createPreContextServices(input) {
25676
25845
  const systemPrompt = await systemPromptBuilder.build({
25677
25846
  cwd: projectRoot,
25678
25847
  projectRoot,
25679
- tools: toolRegistry.list(),
25848
+ tools: toolRegistry.listForProvider(),
25849
+ catalogTools: toolRegistry.list(),
25680
25850
  provider: config.provider,
25681
25851
  model: config.model,
25682
25852
  onlineAgents
@@ -25694,6 +25864,7 @@ async function createPreContextServices(input) {
25694
25864
  projectRoot,
25695
25865
  model: config.model
25696
25866
  });
25867
+ context.meta["promptOnlineAgents"] = onlineAgents;
25697
25868
  const initialContextPolicy = resolveContextWindowPolicy3(config.context);
25698
25869
  context.meta["contextWindowMode"] = initialContextPolicy.id;
25699
25870
  context.meta["contextWindowPolicy"] = initialContextPolicy;
@@ -25766,6 +25937,7 @@ function createModeHandlers(context) {
25766
25937
  modeId: id,
25767
25938
  modePrompt,
25768
25939
  modelCapabilities: context.modelCapabilities,
25940
+ tokenSavingMode: config.features?.tokenSavingMode,
25769
25941
  instructionPaths: {
25770
25942
  globalDir: paths.globalInstructions,
25771
25943
  projectDir: paths.inProjectInstructions,
@@ -25775,7 +25947,8 @@ function createModeHandlers(context) {
25775
25947
  context.context.systemPrompt = await builder.build({
25776
25948
  cwd: context.projectRoot,
25777
25949
  projectRoot: context.projectRoot,
25778
- tools: context.toolRegistry.list(),
25950
+ tools: context.toolRegistry.listForProvider(),
25951
+ catalogTools: context.toolRegistry.list(),
25779
25952
  provider: config.provider,
25780
25953
  model: config.model
25781
25954
  });
@@ -27050,15 +27223,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
27050
27223
  eternalSubscription = null;
27051
27224
  }
27052
27225
  codebaseIndexing.dispose();
27053
- if (config.Sage?.enabled !== false && config.Sage?.hygiene?.autoAfterSession !== false) {
27054
- const candidate = memoryStore;
27055
- await candidate.hygiene?.({
27056
- retentionDays: config.Sage?.hygiene?.retentionDays,
27057
- archiveLowConfidenceAfterDays: config.Sage?.hygiene?.archiveLowConfidenceAfterDays
27058
- }).catch(
27059
- (err) => logger.warn(`sage session hygiene failed: ${toErrorMessage14(err)}`)
27060
- );
27061
- }
27226
+ await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage14(err)}`));
27062
27227
  await memoryStore.dispose().catch(
27063
27228
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
27064
27229
  );