@wrongstack/webui-server 0.306.2 → 0.306.4

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
@@ -1210,6 +1210,102 @@ function patchConfig(config, updates) {
1210
1210
  return Object.freeze({ ...config, ...updates });
1211
1211
  }
1212
1212
 
1213
+ // src/server/session-cleanup-scheduler.ts
1214
+ import { toErrorMessage } from "@wrongstack/core/utils";
1215
+
1216
+ // src/server/session-deletion.ts
1217
+ async function deleteWebUISession(ctx, sessionId) {
1218
+ if (sessionId === ctx.getActiveSessionId()) {
1219
+ throw new Error("Cannot delete the active session");
1220
+ }
1221
+ await ctx.getSessionStore().delete(sessionId);
1222
+ await ctx.refreshSessions().catch(() => void 0);
1223
+ }
1224
+
1225
+ // src/server/session-cleanup-scheduler.ts
1226
+ var DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
1227
+ var EMPTY_SESSION_CLEANUP_INTERVAL_ENV = "WRONGSTACK_EMPTY_SESSION_CLEANUP_INTERVAL_MS";
1228
+ var MAX_SESSION_SCAN = 1e3;
1229
+ function resolveEmptySessionCleanupInterval(value = process.env[EMPTY_SESSION_CLEANUP_INTERVAL_ENV]) {
1230
+ if (value === void 0 || value.trim() === "") return DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS;
1231
+ const parsed = Number(value);
1232
+ return Number.isFinite(parsed) && parsed >= 1e3 ? Math.floor(parsed) : DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS;
1233
+ }
1234
+ async function cleanupOwnerlessEmptySessions(ctx) {
1235
+ const store = ctx.getSessionStore();
1236
+ let deleted = 0;
1237
+ let errors = 0;
1238
+ let sessions;
1239
+ try {
1240
+ sessions = await store.list(MAX_SESSION_SCAN);
1241
+ } catch (error2) {
1242
+ errors++;
1243
+ ctx.logger.error("Empty session cleanup failed to list sessions", {
1244
+ event: "webui.empty_session_cleanup_error",
1245
+ phase: "list",
1246
+ message: toErrorMessage(error2)
1247
+ });
1248
+ ctx.logger.info("Empty session cleanup completed", {
1249
+ event: "webui.empty_session_cleanup_completed",
1250
+ deleted,
1251
+ errors
1252
+ });
1253
+ return { deleted, errors };
1254
+ }
1255
+ for (const candidate of sessions) {
1256
+ const sessionId = candidate.id;
1257
+ if (sessionId === ctx.getActiveSessionId() || ctx.hasParticipants(sessionId)) continue;
1258
+ try {
1259
+ if (!store.isEmpty || !await store.isEmpty(sessionId)) continue;
1260
+ if (store !== ctx.getSessionStore() || sessionId === ctx.getActiveSessionId() || ctx.hasParticipants(sessionId)) {
1261
+ continue;
1262
+ }
1263
+ await deleteWebUISession(
1264
+ {
1265
+ getActiveSessionId: ctx.getActiveSessionId,
1266
+ getSessionStore: () => store,
1267
+ refreshSessions: ctx.refreshSessions
1268
+ },
1269
+ sessionId
1270
+ );
1271
+ deleted++;
1272
+ } catch (error2) {
1273
+ errors++;
1274
+ ctx.logger.error("Empty session cleanup failed for session", {
1275
+ event: "webui.empty_session_cleanup_error",
1276
+ phase: "delete",
1277
+ sessionId,
1278
+ message: toErrorMessage(error2)
1279
+ });
1280
+ }
1281
+ }
1282
+ ctx.logger.info("Empty session cleanup completed", {
1283
+ event: "webui.empty_session_cleanup_completed",
1284
+ deleted,
1285
+ errors
1286
+ });
1287
+ return { deleted, errors };
1288
+ }
1289
+ function scheduleOwnerlessEmptySessionCleanup(ctx, intervalMs = resolveEmptySessionCleanupInterval()) {
1290
+ let running = null;
1291
+ const runNow = () => {
1292
+ if (running) return running;
1293
+ running = cleanupOwnerlessEmptySessions(ctx).finally(() => {
1294
+ running = null;
1295
+ });
1296
+ return running;
1297
+ };
1298
+ const timer = setInterval(() => void runNow(), intervalMs);
1299
+ timer.unref?.();
1300
+ return {
1301
+ dispose: async () => {
1302
+ clearInterval(timer);
1303
+ await running;
1304
+ },
1305
+ runNow
1306
+ };
1307
+ }
1308
+
1213
1309
  // src/server/brain-routes.ts
1214
1310
  async function handleBrainRoute(ws, msg, handlers) {
1215
1311
  switch (msg.type) {
@@ -1757,7 +1853,7 @@ function extractCodeMapFileTargets(projectRoot, toolName2, rawInput) {
1757
1853
 
1758
1854
  // src/server/collaboration-ws-handler.ts
1759
1855
  import { randomUUID } from "node:crypto";
1760
- import { toErrorMessage } from "@wrongstack/core/utils";
1856
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
1761
1857
  var REPLAY_LIMIT = 50;
1762
1858
  var PAUSE_TIMEOUT_MS = 6e4;
1763
1859
  var CollaborationWebSocketHandler = class {
@@ -1792,6 +1888,10 @@ var CollaborationWebSocketHandler = class {
1792
1888
  ws.on("close", () => this.handleDisconnect(ws));
1793
1889
  ws.on("error", () => this.handleDisconnect(ws));
1794
1890
  }
1891
+ /** True while at least one collaboration participant is attached to a session. */
1892
+ hasParticipants(sessionId) {
1893
+ return (this.bySession.get(sessionId)?.size ?? 0) > 0;
1894
+ }
1795
1895
  dispose() {
1796
1896
  for (const off of this.offs) off();
1797
1897
  this.offs.length = 0;
@@ -1909,7 +2009,7 @@ var CollaborationWebSocketHandler = class {
1909
2009
  this.broadcast(sessionId, this.stateMessage(sessionId));
1910
2010
  if (this.reader) {
1911
2011
  this.replayHistory(ws, sessionId).catch((err) => {
1912
- this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
2012
+ this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage2(err)}`);
1913
2013
  });
1914
2014
  }
1915
2015
  this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
@@ -2016,7 +2116,7 @@ var CollaborationWebSocketHandler = class {
2016
2116
  }
2017
2117
  });
2018
2118
  } catch (err) {
2019
- this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
2119
+ this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage2(err)}`));
2020
2120
  }
2021
2121
  }
2022
2122
  async handleResolve(ws, raw) {
@@ -2068,7 +2168,7 @@ var CollaborationWebSocketHandler = class {
2068
2168
  }
2069
2169
  });
2070
2170
  } catch (err) {
2071
- this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
2171
+ this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage2(err)}`));
2072
2172
  }
2073
2173
  }
2074
2174
  // ── Event subscription (live mirror) ───────────────────────────────────
@@ -2139,7 +2239,7 @@ var CollaborationWebSocketHandler = class {
2139
2239
  seen++;
2140
2240
  }
2141
2241
  } catch (err) {
2142
- this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
2242
+ this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage2(err)}`);
2143
2243
  return;
2144
2244
  }
2145
2245
  const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
@@ -2222,7 +2322,7 @@ var CollaborationWebSocketHandler = class {
2222
2322
  try {
2223
2323
  sendSerialized(p.ws, data);
2224
2324
  } catch (err) {
2225
- this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
2325
+ this.logger.debug?.(`collab broadcast failed: ${toErrorMessage2(err)}`);
2226
2326
  }
2227
2327
  }
2228
2328
  }
@@ -6910,7 +7010,7 @@ import {
6910
7010
  PhaseOrchestrator,
6911
7011
  PhaseStore
6912
7012
  } from "@wrongstack/core/goal";
6913
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
7013
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
6914
7014
  import { WorktreeManager } from "@wrongstack/core/worktree";
6915
7015
 
6916
7016
  // src/server/git-process.ts
@@ -7163,7 +7263,7 @@ var GoalWebSocketHandler = class {
7163
7263
  sendResult7(result);
7164
7264
  } catch (err) {
7165
7265
  if (mySeq !== this.assessSeq) return;
7166
- this.logger.error(`[Goal] Assessment failed: ${toErrorMessage2(err)}`);
7266
+ this.logger.error(`[Goal] Assessment failed: ${toErrorMessage3(err)}`);
7167
7267
  sendResult7({
7168
7268
  realistic: true,
7169
7269
  durationClaimed: null,
@@ -7172,7 +7272,7 @@ var GoalWebSocketHandler = class {
7172
7272
  concerns: [],
7173
7273
  raw: "",
7174
7274
  parseFailed: true,
7175
- parseError: `Assessment error: ${toErrorMessage2(err)}`
7275
+ parseError: `Assessment error: ${toErrorMessage3(err)}`
7176
7276
  });
7177
7277
  }
7178
7278
  }
@@ -7303,7 +7403,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
7303
7403
  );
7304
7404
  this.broadcastState();
7305
7405
  }).catch((err) => {
7306
- this.logger.error(`[Goal] Aborted: ${toErrorMessage2(err)}`);
7406
+ this.logger.error(`[Goal] Aborted: ${toErrorMessage3(err)}`);
7307
7407
  this.stopBroadcast();
7308
7408
  this.broadcast({ type: "goal.failed", payload: { title, error: String(err) } });
7309
7409
  });
@@ -7429,7 +7529,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
7429
7529
  }
7430
7530
  this.logger.info(`[Goal] Planner produced no phases; using defaults for: ${goal}`);
7431
7531
  } catch (err) {
7432
- this.logger.error(`[Goal] Planning failed, using defaults: ${toErrorMessage2(err)}`);
7532
+ this.logger.error(`[Goal] Planning failed, using defaults: ${toErrorMessage3(err)}`);
7433
7533
  }
7434
7534
  return this.defaultPhases();
7435
7535
  }
@@ -7490,7 +7590,7 @@ ${result_.finalText.slice(0, 2e3)}`
7490
7590
  );
7491
7591
  }
7492
7592
  } catch (err) {
7493
- this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
7593
+ this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage3(err)}`);
7494
7594
  }
7495
7595
  }
7496
7596
  /**
@@ -9877,6 +9977,16 @@ function createHttpServer(opts) {
9877
9977
  res.end("Unauthorized");
9878
9978
  return;
9879
9979
  }
9980
+ const isMutatingApiRequest = url.pathname.startsWith("/api/") && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH" || req.method === "DELETE");
9981
+ if (isMutatingApiRequest && !req.headers.origin && !accessTokenOk) {
9982
+ res.writeHead(401, { "Content-Type": "application/json", "Cache-Control": "no-store" });
9983
+ res.end(
9984
+ JSON.stringify({
9985
+ error: "Unauthorized: a request without an Origin header must present the access token (X-WS-Token header or ws_token cookie)."
9986
+ })
9987
+ );
9988
+ return;
9989
+ }
9880
9990
  if (shouldSetAuthCookie && opts.apiToken) {
9881
9991
  setAuthCookieHeaders(res, opts.apiToken, secureCookies);
9882
9992
  }
@@ -13800,7 +13910,7 @@ async function handleMemoryRoute(ctx, ws, message) {
13800
13910
 
13801
13911
  // src/server/mode-operations.ts
13802
13912
  import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
13803
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
13913
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
13804
13914
  function sendResult4(context, ws, success, message) {
13805
13915
  context.send(ws, { type: "key.operation_result", payload: { success, message } });
13806
13916
  }
@@ -13832,7 +13942,7 @@ function createModeOperations(context) {
13832
13942
  } catch (error2) {
13833
13943
  context.send(ws, {
13834
13944
  type: "modes.list",
13835
- payload: { modes: [], activeId: "default", error: toErrorMessage3(error2) }
13945
+ payload: { modes: [], activeId: "default", error: toErrorMessage4(error2) }
13836
13946
  });
13837
13947
  }
13838
13948
  },
@@ -13864,7 +13974,7 @@ function createModeOperations(context) {
13864
13974
  await context.afterSwitch?.(id);
13865
13975
  sendResult4(context, ws, true, `Switched to mode "${id}"`);
13866
13976
  } catch (error2) {
13867
- sendResult4(context, ws, false, toErrorMessage3(error2));
13977
+ sendResult4(context, ws, false, toErrorMessage4(error2));
13868
13978
  }
13869
13979
  }
13870
13980
  };
@@ -13911,7 +14021,7 @@ import {
13911
14021
  resolveConfiguredRefinerRef,
13912
14022
  resolveEnhanceFallbackRef
13913
14023
  } from "@wrongstack/core/execution";
13914
- import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
14024
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
13915
14025
  function sendResult5(context, ws, payload, legacyResult = false) {
13916
14026
  const message = { type: "model.switch_result", payload };
13917
14027
  if (payload.success && context.broadcast) context.broadcast(message);
@@ -13969,7 +14079,7 @@ function createModelOperations(context) {
13969
14079
  {
13970
14080
  ...requestId ? { requestId } : {},
13971
14081
  success: false,
13972
- message: `Switch failed: ${toErrorMessage4(error2)}`,
14082
+ message: `Switch failed: ${toErrorMessage5(error2)}`,
13973
14083
  provider,
13974
14084
  model,
13975
14085
  previousProvider,
@@ -14016,7 +14126,7 @@ function createModelOperations(context) {
14016
14126
  payload: {
14017
14127
  refined: text2,
14018
14128
  english: text2,
14019
- error: `Cannot use ${payload.provider}/${payload.model}: ${toErrorMessage4(error2)}`,
14129
+ error: `Cannot use ${payload.provider}/${payload.model}: ${toErrorMessage5(error2)}`,
14020
14130
  errorKind: "provider_error",
14021
14131
  ...fallbackRef ? { fallbackRef } : {}
14022
14132
  }
@@ -14114,7 +14224,7 @@ function createModelOperations(context) {
14114
14224
  JSON.stringify({
14115
14225
  level: "error",
14116
14226
  event: "model.refine.error",
14117
- error: toErrorMessage4(error2),
14227
+ error: toErrorMessage5(error2),
14118
14228
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
14119
14229
  })
14120
14230
  );
@@ -14123,7 +14233,7 @@ function createModelOperations(context) {
14123
14233
  payload: {
14124
14234
  refined: text2,
14125
14235
  english: text2,
14126
- error: toErrorMessage4(error2),
14236
+ error: toErrorMessage5(error2),
14127
14237
  errorKind: "provider_error",
14128
14238
  ...fallbackRef ? { fallbackRef } : {}
14129
14239
  }
@@ -14812,7 +14922,7 @@ function createWebuiClientPresence(deps2) {
14812
14922
 
14813
14923
  // src/server/brain-handlers.ts
14814
14924
  import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
14815
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
14925
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
14816
14926
  var COUNCIL_PERSONA_CATALOG = Object.freeze(
14817
14927
  BUILTIN_COUNCIL_PERSONAS.map(
14818
14928
  (persona) => Object.freeze({
@@ -14901,7 +15011,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
14901
15011
  payload: {
14902
15012
  config: brainConfigPayload(ctx.brainRuntime),
14903
15013
  persisted: false,
14904
- error: `Invalid Brain setting: ${toErrorMessage5(err)}`
15014
+ error: `Invalid Brain setting: ${toErrorMessage6(err)}`
14905
15015
  }
14906
15016
  });
14907
15017
  }
@@ -14947,7 +15057,7 @@ async function handleBrainAsk(ctx, ws, question) {
14947
15057
  payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
14948
15058
  });
14949
15059
  } catch (err) {
14950
- sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage5(err)}`);
15060
+ sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
14951
15061
  }
14952
15062
  }
14953
15063
 
@@ -15734,7 +15844,6 @@ function seedContextMeta(config, context) {
15734
15844
  const hqConfig = config.hq;
15735
15845
  meta["hqEnabled"] = hqConfig?.enabled === true;
15736
15846
  meta["hqUrl"] = hqConfig?.url ?? "";
15737
- meta["hqToken"] = hqConfig?.token ?? "";
15738
15847
  meta["hqRawContent"] = hqConfig?.rawContent === true;
15739
15848
  const tgExt = config.extensions?.["telegram"];
15740
15849
  meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
@@ -15828,7 +15937,6 @@ var PREF_KEYS = [
15828
15937
  "auditLevel",
15829
15938
  "hqEnabled",
15830
15939
  "hqUrl",
15831
- "hqToken",
15832
15940
  "hqRawContent",
15833
15941
  "tgConfigured",
15834
15942
  "tgSessionEnd",
@@ -16390,7 +16498,7 @@ import * as fs16 from "node:fs/promises";
16390
16498
  import * as path20 from "node:path";
16391
16499
  import { TOKENS } from "@wrongstack/core/kernel";
16392
16500
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
16393
- import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
16501
+ import { toErrorMessage as toErrorMessage8, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
16394
16502
  import { makeProviderFromConfig } from "@wrongstack/providers";
16395
16503
 
16396
16504
  // src/server/project-handlers.ts
@@ -16633,7 +16741,8 @@ function createProjectHandlers(ctx) {
16633
16741
  // src/server/provider-handlers.ts
16634
16742
  import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
16635
16743
  import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
16636
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
16744
+ import { validateProviderBaseUrl } from "@wrongstack/core/tools";
16745
+ import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
16637
16746
  import {
16638
16747
  beginOAuthLogin
16639
16748
  } from "@wrongstack/providers/oauth";
@@ -17221,6 +17330,13 @@ function createProviderOperations(deps2) {
17221
17330
  sendOperationResult(ws, false, `Unknown provider "${payload.id}"`);
17222
17331
  return;
17223
17332
  }
17333
+ if (payload.baseUrl !== void 0 && payload.baseUrl !== "") {
17334
+ const invalid = validateProviderBaseUrl(payload.baseUrl);
17335
+ if (invalid) {
17336
+ sendOperationResult(ws, false, invalid);
17337
+ return;
17338
+ }
17339
+ }
17224
17340
  if (payload.family !== void 0) cfg.family = payload.family;
17225
17341
  if (payload.baseUrl !== void 0) cfg.baseUrl = payload.baseUrl;
17226
17342
  if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
@@ -17401,7 +17517,7 @@ function createProviderHandlers(deps2) {
17401
17517
  JSON.stringify({
17402
17518
  level: "error",
17403
17519
  event: "webui.provider_save_failed",
17404
- message: toErrorMessage6(error2),
17520
+ message: toErrorMessage7(error2),
17405
17521
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17406
17522
  })
17407
17523
  );
@@ -19057,21 +19173,21 @@ function createSessionHandlers(ctx) {
19057
19173
  deleteSession: async (ws, msg) => {
19058
19174
  const { id } = msg.payload;
19059
19175
  try {
19060
- if (id === ctx.getSession().id) {
19061
- result(ws, false, "Cannot delete the active session");
19062
- return;
19063
- }
19064
- const store = ctx.getSessionStore();
19065
- await store.delete(id);
19176
+ await deleteWebUISession(
19177
+ {
19178
+ getActiveSessionId: () => ctx.getSession().id,
19179
+ getSessionStore: ctx.getSessionStore,
19180
+ refreshSessions: async () => {
19181
+ const list = await ctx.getSessionStore().list(200);
19182
+ broadcastToAll({
19183
+ type: "sessions.list",
19184
+ payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
19185
+ });
19186
+ }
19187
+ },
19188
+ id
19189
+ );
19066
19190
  result(ws, true, `Session ${id} deleted`);
19067
- try {
19068
- const list = await store.list(200);
19069
- broadcastToAll({
19070
- type: "sessions.list",
19071
- payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
19072
- });
19073
- } catch {
19074
- }
19075
19191
  } catch (err) {
19076
19192
  result(ws, false, errMessage(err));
19077
19193
  }
@@ -19254,7 +19370,7 @@ async function applyEmbeddedModelSwitch(ctx, providerId, modelId) {
19254
19370
  event: "models.refresh_failed",
19255
19371
  provider: providerId,
19256
19372
  model: modelId,
19257
- message: toErrorMessage7(error2),
19373
+ message: toErrorMessage8(error2),
19258
19374
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
19259
19375
  })
19260
19376
  );
@@ -23376,7 +23492,7 @@ import {
23376
23492
  expectDefined as expectDefined4,
23377
23493
  sessionScopedPath as sessionScopedPath3,
23378
23494
  startSharedHeapWatchdog,
23379
- toErrorMessage as toErrorMessage14,
23495
+ toErrorMessage as toErrorMessage15,
23380
23496
  wstackGlobalRoot as wstackGlobalRoot4
23381
23497
  } from "@wrongstack/core/utils";
23382
23498
  import { makeProviderFromConfig as makeProviderFromConfig5 } from "@wrongstack/providers";
@@ -23413,7 +23529,7 @@ import {
23413
23529
  } from "@wrongstack/core/types";
23414
23530
  import {
23415
23531
  estimateRequestTokensCalibrated,
23416
- toErrorMessage as toErrorMessage10
23532
+ toErrorMessage as toErrorMessage11
23417
23533
  } from "@wrongstack/core/utils";
23418
23534
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
23419
23535
  import { getSageService, setupSage } from "@wrongstack/sage";
@@ -23569,7 +23685,7 @@ function sleep(ms) {
23569
23685
  import { spawn as spawnChild } from "node:child_process";
23570
23686
  import { createRequire as createRequire2 } from "node:module";
23571
23687
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary2 } from "@wrongstack/core/security";
23572
- import { buildChildEnv, toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
23688
+ import { buildChildEnv, toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
23573
23689
  var MAX_SESSIONS_PER_CLIENT = 8;
23574
23690
  var MAX_INPUT_BYTES = 1 << 16;
23575
23691
  var DEFAULT_COLS = 80;
@@ -23685,8 +23801,8 @@ var TerminalWebSocketHandler = class {
23685
23801
  ...windowsPtyOptions()
23686
23802
  });
23687
23803
  } catch (err) {
23688
- const msg = `Integrated terminal failed to start: ${toErrorMessage8(err)}`;
23689
- this.logger.warn?.(`terminal spawn failed: ${toErrorMessage8(err)}`);
23804
+ const msg = `Integrated terminal failed to start: ${toErrorMessage9(err)}`;
23805
+ this.logger.warn?.(`terminal spawn failed: ${toErrorMessage9(err)}`);
23690
23806
  this.send(ws, { type: "terminal.output", payload: { id: payload.id, data: `${msg}\r
23691
23807
  ` } });
23692
23808
  this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
@@ -23747,7 +23863,7 @@ var TerminalWebSocketHandler = class {
23747
23863
  try {
23748
23864
  pty.kill();
23749
23865
  } catch (err) {
23750
- this.logger.warn?.(`${reason} failed: ${toErrorMessage8(err)}`);
23866
+ this.logger.warn?.(`${reason} failed: ${toErrorMessage9(err)}`);
23751
23867
  }
23752
23868
  }
23753
23869
  killWindowsProcessTree(pty, reason) {
@@ -23759,7 +23875,7 @@ var TerminalWebSocketHandler = class {
23759
23875
  } catch {
23760
23876
  }
23761
23877
  } catch (err) {
23762
- this.logger.warn?.(`${reason} taskkill failed: ${toErrorMessage8(err)}`);
23878
+ this.logger.warn?.(`${reason} taskkill failed: ${toErrorMessage9(err)}`);
23763
23879
  }
23764
23880
  }
23765
23881
  send(ws, msg) {
@@ -23806,7 +23922,7 @@ function clampDim(value, fallback) {
23806
23922
 
23807
23923
  // src/server/worktree-ws-handler.ts
23808
23924
  import { join as join14, resolve as resolve14, sep as sep5 } from "node:path";
23809
- import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
23925
+ import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
23810
23926
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
23811
23927
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
23812
23928
  var MAX_ACTIVITY = 6;
@@ -23972,7 +24088,7 @@ var WorktreeWebSocketHandler = class {
23972
24088
  }
23973
24089
  });
23974
24090
  } catch (err) {
23975
- this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage9(err)}`);
24091
+ this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage10(err)}`);
23976
24092
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
23977
24093
  }
23978
24094
  }
@@ -24234,7 +24350,7 @@ var WorktreeWebSocketHandler = class {
24234
24350
  try {
24235
24351
  sendSerialized(ws, data);
24236
24352
  } catch (err) {
24237
- this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage9(err)}`);
24353
+ this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage10(err)}`);
24238
24354
  }
24239
24355
  }
24240
24356
  }
@@ -24343,7 +24459,7 @@ async function createAgentServices(input) {
24343
24459
  const updateAutoCompactionMaxContext = async (newProvider, providerId = newProvider.id, providerCfg) => {
24344
24460
  await modelsRegistry.refresh().catch((err) => {
24345
24461
  logger.warn(
24346
- `models.dev refresh failed for ${providerId}/${context.model}: ${toErrorMessage10(err)}; using cached catalog`
24462
+ `models.dev refresh failed for ${providerId}/${context.model}: ${toErrorMessage11(err)}; using cached catalog`
24347
24463
  );
24348
24464
  });
24349
24465
  const currentConfig = input.config;
@@ -25185,7 +25301,7 @@ import {
25185
25301
  import {
25186
25302
  configureChildEnvGitIdentity,
25187
25303
  sessionScopedPath as sessionScopedPath2,
25188
- toErrorMessage as toErrorMessage12
25304
+ toErrorMessage as toErrorMessage13
25189
25305
  } from "@wrongstack/core/utils";
25190
25306
  import {
25191
25307
  createVaultBackedMcpAuthorizationProviderFactory,
@@ -25267,7 +25383,7 @@ async function discoverAndMergeWebuiProviders(opts) {
25267
25383
 
25268
25384
  // src/server/setup-screen.ts
25269
25385
  import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
25270
- import { toErrorMessage as toErrorMessage11 } from "@wrongstack/core/utils";
25386
+ import { toErrorMessage as toErrorMessage12 } from "@wrongstack/core/utils";
25271
25387
  import { makeProviderFromConfig as makeProviderFromConfig3 } from "@wrongstack/providers";
25272
25388
  var UNCONFIGURED_CAPABILITIES = {
25273
25389
  tools: false,
@@ -25308,7 +25424,7 @@ function logCreateFailure(event, err) {
25308
25424
  JSON.stringify({
25309
25425
  level: "error",
25310
25426
  event,
25311
- message: toErrorMessage11(err),
25427
+ message: toErrorMessage12(err),
25312
25428
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25313
25429
  })
25314
25430
  );
@@ -25617,7 +25733,7 @@ async function createPreContextServices(input) {
25617
25733
  await modelsRegistry.refresh();
25618
25734
  logger.info("models.dev catalog refreshed");
25619
25735
  } catch (err) {
25620
- logger.warn(`models.dev refresh failed (${toErrorMessage12(err)}); using cached catalog`);
25736
+ logger.warn(`models.dev refresh failed (${toErrorMessage13(err)}); using cached catalog`);
25621
25737
  }
25622
25738
  }
25623
25739
  try {
@@ -25628,7 +25744,7 @@ async function createPreContextServices(input) {
25628
25744
  logger
25629
25745
  });
25630
25746
  } catch (err) {
25631
- logger.debug(`provider auto-discovery skipped: ${toErrorMessage12(err)}`);
25747
+ logger.debug(`provider auto-discovery skipped: ${toErrorMessage13(err)}`);
25632
25748
  }
25633
25749
  try {
25634
25750
  await installCatalogModelOutputLimits({
@@ -25637,7 +25753,7 @@ async function createPreContextServices(input) {
25637
25753
  log: (message) => logger.debug(message)
25638
25754
  });
25639
25755
  } catch (err) {
25640
- logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
25756
+ logger.debug(`model output-limit index skipped: ${toErrorMessage13(err)}`);
25641
25757
  }
25642
25758
  const events = opts.services?.events ?? new EventBus();
25643
25759
  events.setLogger(logger);
@@ -25656,7 +25772,7 @@ async function createPreContextServices(input) {
25656
25772
  JSON.stringify({
25657
25773
  level: "warn",
25658
25774
  event: "webui.provider_registry_load_failed",
25659
- message: toErrorMessage12(err),
25775
+ message: toErrorMessage13(err),
25660
25776
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25661
25777
  })
25662
25778
  );
@@ -26291,7 +26407,7 @@ function buildRoutes(state, deps2, cb) {
26291
26407
  import { createRequire as createRequire4 } from "node:module";
26292
26408
  import * as path33 from "node:path";
26293
26409
  import { fileURLToPath } from "node:url";
26294
- import { toErrorMessage as toErrorMessage13 } from "@wrongstack/core/utils";
26410
+ import { toErrorMessage as toErrorMessage14 } from "@wrongstack/core/utils";
26295
26411
  import { WebSocketServer } from "ws";
26296
26412
  async function resolvePorts(opts) {
26297
26413
  const surface = opts.surface ?? "webui";
@@ -26442,7 +26558,7 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
26442
26558
  level: "error",
26443
26559
  event: "webui.ws_server_error",
26444
26560
  host: wsHost,
26445
- message: toErrorMessage13(err),
26561
+ message: toErrorMessage14(err),
26446
26562
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
26447
26563
  })
26448
26564
  );
@@ -27089,7 +27205,7 @@ async function startWebUI(opts = {}) {
27089
27205
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
27090
27206
  } catch (err) {
27091
27207
  console.warn(
27092
- `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage14(err)}`
27208
+ `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage15(err)}`
27093
27209
  );
27094
27210
  }
27095
27211
  },
@@ -27112,6 +27228,19 @@ async function startWebUI(opts = {}) {
27112
27228
  })
27113
27229
  });
27114
27230
  const routes = buildRoutes(state, deps2, cb);
27231
+ const stopEmptySessionCleanup = scheduleOwnerlessEmptySessionCleanup({
27232
+ getSessionStore: state.getSessionStore,
27233
+ getActiveSessionId: () => state.getSession().id,
27234
+ hasParticipants: (sessionId) => collabHandler.hasParticipants(sessionId),
27235
+ refreshSessions: async () => {
27236
+ const list = await state.getSessionStore().list(200);
27237
+ broadcast(clients, {
27238
+ type: "sessions.list",
27239
+ payload: { sessions: toSessionHistoryEntries(list, state.getSession().id) }
27240
+ });
27241
+ },
27242
+ logger
27243
+ });
27115
27244
  let kanbanSupervisorDispose = null;
27116
27245
  const handleMessage = createMessageDispatcher({
27117
27246
  state,
@@ -27196,7 +27325,8 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
27196
27325
  wssPrimary,
27197
27326
  ...wssSecondary ? [wssSecondary] : []
27198
27327
  ],
27199
- onPreShutdown: () => {
27328
+ onPreShutdown: async () => {
27329
+ await stopEmptySessionCleanup.dispose();
27200
27330
  kanbanSupervisorDispose?.();
27201
27331
  kanbanSupervisorDispose = null;
27202
27332
  },
@@ -27223,9 +27353,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
27223
27353
  eternalSubscription = null;
27224
27354
  }
27225
27355
  codebaseIndexing.dispose();
27226
- await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage14(err)}`));
27356
+ await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage15(err)}`));
27227
27357
  await memoryStore.dispose().catch(
27228
- (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
27358
+ (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage15(err)}`)
27229
27359
  );
27230
27360
  await unregisterInstance(process.pid, path34.dirname(globalConfigPath));
27231
27361
  }
@@ -27234,7 +27364,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
27234
27364
  export {
27235
27365
  CLIENT_MESSAGE_TYPES,
27236
27366
  CollaborationWebSocketHandler,
27367
+ DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS,
27237
27368
  DEFAULT_SURFACE_CONNECTION_CONFIG,
27369
+ EMPTY_SESSION_CLEANUP_INTERVAL_ENV,
27238
27370
  GoalWebSocketHandler,
27239
27371
  KANBAN_CLIENT_MESSAGE_TYPES,
27240
27372
  PREF_KEYS,
@@ -27266,6 +27398,7 @@ export {
27266
27398
  buildTaskGraphFromGoalPhase,
27267
27399
  buildTaskGraphFromSddSnapshot,
27268
27400
  buildWebUIAccessUrl,
27401
+ cleanupOwnerlessEmptySessions,
27269
27402
  clearAnalyticsBuffer,
27270
27403
  collectConnectionsHealth,
27271
27404
  computeUsageCost,
@@ -27309,6 +27442,7 @@ export {
27309
27442
  decodeSessionId,
27310
27443
  defaultBaseDir,
27311
27444
  deleteKey,
27445
+ deleteWebUISession,
27312
27446
  enqueueBounded,
27313
27447
  ensureDistDir,
27314
27448
  ensureProjectDataDir,
@@ -27496,12 +27630,14 @@ export {
27496
27630
  resolveAllPendingConfirms,
27497
27631
  resolveAuthToken,
27498
27632
  resolveDistDir,
27633
+ resolveEmptySessionCleanupInterval,
27499
27634
  resolveProviderCatalogForModels,
27500
27635
  resolveProviderModelMetadata,
27501
27636
  resolveWorkingDirInsideProject,
27502
27637
  resolveYoloEligiblePendingConfirms,
27503
27638
  saveManifest,
27504
27639
  saveProviders,
27640
+ scheduleOwnerlessEmptySessionCleanup,
27505
27641
  seedContextMeta,
27506
27642
  send,
27507
27643
  sendResult2 as sendResult,