@wrongstack/webui-server 0.306.3 → 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.
@@ -95,6 +95,8 @@ export declare class CollaborationWebSocketHandler {
95
95
  */
96
96
  bus?: CollaborationBus | undefined, options?: CollaborationHandlerOptions);
97
97
  addClient(ws: WebSocket): void;
98
+ /** True while at least one collaboration participant is attached to a session. */
99
+ hasParticipants(sessionId: string): boolean;
98
100
  dispose(): void;
99
101
  /**
100
102
  * Dispatch a parsed client message. Returns true when the message was
@@ -1167,6 +1167,102 @@ function patchConfig(config, updates) {
1167
1167
  return Object.freeze({ ...config, ...updates });
1168
1168
  }
1169
1169
 
1170
+ // src/server/session-cleanup-scheduler.ts
1171
+ import { toErrorMessage } from "@wrongstack/core/utils";
1172
+
1173
+ // src/server/session-deletion.ts
1174
+ async function deleteWebUISession(ctx, sessionId) {
1175
+ if (sessionId === ctx.getActiveSessionId()) {
1176
+ throw new Error("Cannot delete the active session");
1177
+ }
1178
+ await ctx.getSessionStore().delete(sessionId);
1179
+ await ctx.refreshSessions().catch(() => void 0);
1180
+ }
1181
+
1182
+ // src/server/session-cleanup-scheduler.ts
1183
+ var DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
1184
+ var EMPTY_SESSION_CLEANUP_INTERVAL_ENV = "WRONGSTACK_EMPTY_SESSION_CLEANUP_INTERVAL_MS";
1185
+ var MAX_SESSION_SCAN = 1e3;
1186
+ function resolveEmptySessionCleanupInterval(value = process.env[EMPTY_SESSION_CLEANUP_INTERVAL_ENV]) {
1187
+ if (value === void 0 || value.trim() === "") return DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS;
1188
+ const parsed = Number(value);
1189
+ return Number.isFinite(parsed) && parsed >= 1e3 ? Math.floor(parsed) : DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS;
1190
+ }
1191
+ async function cleanupOwnerlessEmptySessions(ctx) {
1192
+ const store = ctx.getSessionStore();
1193
+ let deleted = 0;
1194
+ let errors = 0;
1195
+ let sessions;
1196
+ try {
1197
+ sessions = await store.list(MAX_SESSION_SCAN);
1198
+ } catch (error2) {
1199
+ errors++;
1200
+ ctx.logger.error("Empty session cleanup failed to list sessions", {
1201
+ event: "webui.empty_session_cleanup_error",
1202
+ phase: "list",
1203
+ message: toErrorMessage(error2)
1204
+ });
1205
+ ctx.logger.info("Empty session cleanup completed", {
1206
+ event: "webui.empty_session_cleanup_completed",
1207
+ deleted,
1208
+ errors
1209
+ });
1210
+ return { deleted, errors };
1211
+ }
1212
+ for (const candidate of sessions) {
1213
+ const sessionId = candidate.id;
1214
+ if (sessionId === ctx.getActiveSessionId() || ctx.hasParticipants(sessionId)) continue;
1215
+ try {
1216
+ if (!store.isEmpty || !await store.isEmpty(sessionId)) continue;
1217
+ if (store !== ctx.getSessionStore() || sessionId === ctx.getActiveSessionId() || ctx.hasParticipants(sessionId)) {
1218
+ continue;
1219
+ }
1220
+ await deleteWebUISession(
1221
+ {
1222
+ getActiveSessionId: ctx.getActiveSessionId,
1223
+ getSessionStore: () => store,
1224
+ refreshSessions: ctx.refreshSessions
1225
+ },
1226
+ sessionId
1227
+ );
1228
+ deleted++;
1229
+ } catch (error2) {
1230
+ errors++;
1231
+ ctx.logger.error("Empty session cleanup failed for session", {
1232
+ event: "webui.empty_session_cleanup_error",
1233
+ phase: "delete",
1234
+ sessionId,
1235
+ message: toErrorMessage(error2)
1236
+ });
1237
+ }
1238
+ }
1239
+ ctx.logger.info("Empty session cleanup completed", {
1240
+ event: "webui.empty_session_cleanup_completed",
1241
+ deleted,
1242
+ errors
1243
+ });
1244
+ return { deleted, errors };
1245
+ }
1246
+ function scheduleOwnerlessEmptySessionCleanup(ctx, intervalMs = resolveEmptySessionCleanupInterval()) {
1247
+ let running = null;
1248
+ const runNow = () => {
1249
+ if (running) return running;
1250
+ running = cleanupOwnerlessEmptySessions(ctx).finally(() => {
1251
+ running = null;
1252
+ });
1253
+ return running;
1254
+ };
1255
+ const timer = setInterval(() => void runNow(), intervalMs);
1256
+ timer.unref?.();
1257
+ return {
1258
+ dispose: async () => {
1259
+ clearInterval(timer);
1260
+ await running;
1261
+ },
1262
+ runNow
1263
+ };
1264
+ }
1265
+
1170
1266
  // src/server/brain-routes.ts
1171
1267
  async function handleBrainRoute(ws, msg, handlers) {
1172
1268
  switch (msg.type) {
@@ -1714,7 +1810,7 @@ function extractCodeMapFileTargets(projectRoot, toolName2, rawInput) {
1714
1810
 
1715
1811
  // src/server/collaboration-ws-handler.ts
1716
1812
  import { randomUUID } from "node:crypto";
1717
- import { toErrorMessage } from "@wrongstack/core/utils";
1813
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
1718
1814
  var REPLAY_LIMIT = 50;
1719
1815
  var PAUSE_TIMEOUT_MS = 6e4;
1720
1816
  var CollaborationWebSocketHandler = class {
@@ -1749,6 +1845,10 @@ var CollaborationWebSocketHandler = class {
1749
1845
  ws.on("close", () => this.handleDisconnect(ws));
1750
1846
  ws.on("error", () => this.handleDisconnect(ws));
1751
1847
  }
1848
+ /** True while at least one collaboration participant is attached to a session. */
1849
+ hasParticipants(sessionId) {
1850
+ return (this.bySession.get(sessionId)?.size ?? 0) > 0;
1851
+ }
1752
1852
  dispose() {
1753
1853
  for (const off of this.offs) off();
1754
1854
  this.offs.length = 0;
@@ -1866,7 +1966,7 @@ var CollaborationWebSocketHandler = class {
1866
1966
  this.broadcast(sessionId, this.stateMessage(sessionId));
1867
1967
  if (this.reader) {
1868
1968
  this.replayHistory(ws, sessionId).catch((err) => {
1869
- this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
1969
+ this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage2(err)}`);
1870
1970
  });
1871
1971
  }
1872
1972
  this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
@@ -1973,7 +2073,7 @@ var CollaborationWebSocketHandler = class {
1973
2073
  }
1974
2074
  });
1975
2075
  } catch (err) {
1976
- this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
2076
+ this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage2(err)}`));
1977
2077
  }
1978
2078
  }
1979
2079
  async handleResolve(ws, raw) {
@@ -2025,7 +2125,7 @@ var CollaborationWebSocketHandler = class {
2025
2125
  }
2026
2126
  });
2027
2127
  } catch (err) {
2028
- this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
2128
+ this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage2(err)}`));
2029
2129
  }
2030
2130
  }
2031
2131
  // ── Event subscription (live mirror) ───────────────────────────────────
@@ -2096,7 +2196,7 @@ var CollaborationWebSocketHandler = class {
2096
2196
  seen++;
2097
2197
  }
2098
2198
  } catch (err) {
2099
- this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
2199
+ this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage2(err)}`);
2100
2200
  return;
2101
2201
  }
2102
2202
  const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
@@ -2179,7 +2279,7 @@ var CollaborationWebSocketHandler = class {
2179
2279
  try {
2180
2280
  sendSerialized(p.ws, data);
2181
2281
  } catch (err) {
2182
- this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
2282
+ this.logger.debug?.(`collab broadcast failed: ${toErrorMessage2(err)}`);
2183
2283
  }
2184
2284
  }
2185
2285
  }
@@ -6824,7 +6924,7 @@ import {
6824
6924
  PhaseOrchestrator,
6825
6925
  PhaseStore
6826
6926
  } from "@wrongstack/core/goal";
6827
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
6927
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
6828
6928
  import { WorktreeManager } from "@wrongstack/core/worktree";
6829
6929
 
6830
6930
  // src/server/git-process.ts
@@ -7077,7 +7177,7 @@ var GoalWebSocketHandler = class {
7077
7177
  sendResult7(result);
7078
7178
  } catch (err) {
7079
7179
  if (mySeq !== this.assessSeq) return;
7080
- this.logger.error(`[Goal] Assessment failed: ${toErrorMessage2(err)}`);
7180
+ this.logger.error(`[Goal] Assessment failed: ${toErrorMessage3(err)}`);
7081
7181
  sendResult7({
7082
7182
  realistic: true,
7083
7183
  durationClaimed: null,
@@ -7086,7 +7186,7 @@ var GoalWebSocketHandler = class {
7086
7186
  concerns: [],
7087
7187
  raw: "",
7088
7188
  parseFailed: true,
7089
- parseError: `Assessment error: ${toErrorMessage2(err)}`
7189
+ parseError: `Assessment error: ${toErrorMessage3(err)}`
7090
7190
  });
7091
7191
  }
7092
7192
  }
@@ -7217,7 +7317,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
7217
7317
  );
7218
7318
  this.broadcastState();
7219
7319
  }).catch((err) => {
7220
- this.logger.error(`[Goal] Aborted: ${toErrorMessage2(err)}`);
7320
+ this.logger.error(`[Goal] Aborted: ${toErrorMessage3(err)}`);
7221
7321
  this.stopBroadcast();
7222
7322
  this.broadcast({ type: "goal.failed", payload: { title, error: String(err) } });
7223
7323
  });
@@ -7343,7 +7443,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
7343
7443
  }
7344
7444
  this.logger.info(`[Goal] Planner produced no phases; using defaults for: ${goal}`);
7345
7445
  } catch (err) {
7346
- this.logger.error(`[Goal] Planning failed, using defaults: ${toErrorMessage2(err)}`);
7446
+ this.logger.error(`[Goal] Planning failed, using defaults: ${toErrorMessage3(err)}`);
7347
7447
  }
7348
7448
  return this.defaultPhases();
7349
7449
  }
@@ -7404,7 +7504,7 @@ ${result_.finalText.slice(0, 2e3)}`
7404
7504
  );
7405
7505
  }
7406
7506
  } catch (err) {
7407
- this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
7507
+ this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage3(err)}`);
7408
7508
  }
7409
7509
  }
7410
7510
  /**
@@ -9783,6 +9883,16 @@ function createHttpServer(opts) {
9783
9883
  res.end("Unauthorized");
9784
9884
  return;
9785
9885
  }
9886
+ const isMutatingApiRequest = url.pathname.startsWith("/api/") && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH" || req.method === "DELETE");
9887
+ if (isMutatingApiRequest && !req.headers.origin && !accessTokenOk) {
9888
+ res.writeHead(401, { "Content-Type": "application/json", "Cache-Control": "no-store" });
9889
+ res.end(
9890
+ JSON.stringify({
9891
+ error: "Unauthorized: a request without an Origin header must present the access token (X-WS-Token header or ws_token cookie)."
9892
+ })
9893
+ );
9894
+ return;
9895
+ }
9786
9896
  if (shouldSetAuthCookie && opts.apiToken) {
9787
9897
  setAuthCookieHeaders(res, opts.apiToken, secureCookies);
9788
9898
  }
@@ -13560,7 +13670,7 @@ async function handleMemoryRoute(ctx, ws, message) {
13560
13670
 
13561
13671
  // src/server/mode-operations.ts
13562
13672
  import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
13563
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
13673
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
13564
13674
  function sendResult4(context, ws, success, message) {
13565
13675
  context.send(ws, { type: "key.operation_result", payload: { success, message } });
13566
13676
  }
@@ -13592,7 +13702,7 @@ function createModeOperations(context) {
13592
13702
  } catch (error2) {
13593
13703
  context.send(ws, {
13594
13704
  type: "modes.list",
13595
- payload: { modes: [], activeId: "default", error: toErrorMessage3(error2) }
13705
+ payload: { modes: [], activeId: "default", error: toErrorMessage4(error2) }
13596
13706
  });
13597
13707
  }
13598
13708
  },
@@ -13624,7 +13734,7 @@ function createModeOperations(context) {
13624
13734
  await context.afterSwitch?.(id);
13625
13735
  sendResult4(context, ws, true, `Switched to mode "${id}"`);
13626
13736
  } catch (error2) {
13627
- sendResult4(context, ws, false, toErrorMessage3(error2));
13737
+ sendResult4(context, ws, false, toErrorMessage4(error2));
13628
13738
  }
13629
13739
  }
13630
13740
  };
@@ -13671,7 +13781,7 @@ import {
13671
13781
  resolveConfiguredRefinerRef,
13672
13782
  resolveEnhanceFallbackRef
13673
13783
  } from "@wrongstack/core/execution";
13674
- import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
13784
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
13675
13785
  function sendResult5(context, ws, payload, legacyResult = false) {
13676
13786
  const message = { type: "model.switch_result", payload };
13677
13787
  if (payload.success && context.broadcast) context.broadcast(message);
@@ -13729,7 +13839,7 @@ function createModelOperations(context) {
13729
13839
  {
13730
13840
  ...requestId ? { requestId } : {},
13731
13841
  success: false,
13732
- message: `Switch failed: ${toErrorMessage4(error2)}`,
13842
+ message: `Switch failed: ${toErrorMessage5(error2)}`,
13733
13843
  provider,
13734
13844
  model,
13735
13845
  previousProvider,
@@ -13776,7 +13886,7 @@ function createModelOperations(context) {
13776
13886
  payload: {
13777
13887
  refined: text,
13778
13888
  english: text,
13779
- error: `Cannot use ${payload.provider}/${payload.model}: ${toErrorMessage4(error2)}`,
13889
+ error: `Cannot use ${payload.provider}/${payload.model}: ${toErrorMessage5(error2)}`,
13780
13890
  errorKind: "provider_error",
13781
13891
  ...fallbackRef ? { fallbackRef } : {}
13782
13892
  }
@@ -13874,7 +13984,7 @@ function createModelOperations(context) {
13874
13984
  JSON.stringify({
13875
13985
  level: "error",
13876
13986
  event: "model.refine.error",
13877
- error: toErrorMessage4(error2),
13987
+ error: toErrorMessage5(error2),
13878
13988
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
13879
13989
  })
13880
13990
  );
@@ -13883,7 +13993,7 @@ function createModelOperations(context) {
13883
13993
  payload: {
13884
13994
  refined: text,
13885
13995
  english: text,
13886
- error: toErrorMessage4(error2),
13996
+ error: toErrorMessage5(error2),
13887
13997
  errorKind: "provider_error",
13888
13998
  ...fallbackRef ? { fallbackRef } : {}
13889
13999
  }
@@ -14002,7 +14112,7 @@ function formatExternalAccessUrls(opts) {
14002
14112
 
14003
14113
  // src/server/brain-handlers.ts
14004
14114
  import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
14005
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
14115
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
14006
14116
  var COUNCIL_PERSONA_CATALOG = Object.freeze(
14007
14117
  BUILTIN_COUNCIL_PERSONAS.map(
14008
14118
  (persona) => Object.freeze({
@@ -14091,7 +14201,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
14091
14201
  payload: {
14092
14202
  config: brainConfigPayload(ctx.brainRuntime),
14093
14203
  persisted: false,
14094
- error: `Invalid Brain setting: ${toErrorMessage5(err)}`
14204
+ error: `Invalid Brain setting: ${toErrorMessage6(err)}`
14095
14205
  }
14096
14206
  });
14097
14207
  }
@@ -14137,7 +14247,7 @@ async function handleBrainAsk(ctx, ws, question) {
14137
14247
  payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
14138
14248
  });
14139
14249
  } catch (err) {
14140
- sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage5(err)}`);
14250
+ sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
14141
14251
  }
14142
14252
  }
14143
14253
 
@@ -14530,7 +14640,6 @@ function seedContextMeta(config, context) {
14530
14640
  const hqConfig = config.hq;
14531
14641
  meta["hqEnabled"] = hqConfig?.enabled === true;
14532
14642
  meta["hqUrl"] = hqConfig?.url ?? "";
14533
- meta["hqToken"] = hqConfig?.token ?? "";
14534
14643
  meta["hqRawContent"] = hqConfig?.rawContent === true;
14535
14644
  const tgExt = config.extensions?.["telegram"];
14536
14645
  meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
@@ -14624,7 +14733,6 @@ var PREF_KEYS = [
14624
14733
  "auditLevel",
14625
14734
  "hqEnabled",
14626
14735
  "hqUrl",
14627
- "hqToken",
14628
14736
  "hqRawContent",
14629
14737
  "tgConfigured",
14630
14738
  "tgSessionEnd",
@@ -15421,7 +15529,8 @@ function createProjectHandlers(ctx) {
15421
15529
  // src/server/provider-handlers.ts
15422
15530
  import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
15423
15531
  import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
15424
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
15532
+ import { validateProviderBaseUrl } from "@wrongstack/core/tools";
15533
+ import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
15425
15534
  import {
15426
15535
  beginOAuthLogin
15427
15536
  } from "@wrongstack/providers/oauth";
@@ -16009,6 +16118,13 @@ function createProviderOperations(deps2) {
16009
16118
  sendOperationResult(ws, false, `Unknown provider "${payload.id}"`);
16010
16119
  return;
16011
16120
  }
16121
+ if (payload.baseUrl !== void 0 && payload.baseUrl !== "") {
16122
+ const invalid = validateProviderBaseUrl(payload.baseUrl);
16123
+ if (invalid) {
16124
+ sendOperationResult(ws, false, invalid);
16125
+ return;
16126
+ }
16127
+ }
16012
16128
  if (payload.family !== void 0) cfg.family = payload.family;
16013
16129
  if (payload.baseUrl !== void 0) cfg.baseUrl = payload.baseUrl;
16014
16130
  if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
@@ -16189,7 +16305,7 @@ function createProviderHandlers(deps2) {
16189
16305
  JSON.stringify({
16190
16306
  level: "error",
16191
16307
  event: "webui.provider_save_failed",
16192
- message: toErrorMessage6(error2),
16308
+ message: toErrorMessage7(error2),
16193
16309
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
16194
16310
  })
16195
16311
  );
@@ -17591,21 +17707,21 @@ function createSessionHandlers(ctx) {
17591
17707
  deleteSession: async (ws, msg) => {
17592
17708
  const { id } = msg.payload;
17593
17709
  try {
17594
- if (id === ctx.getSession().id) {
17595
- result(ws, false, "Cannot delete the active session");
17596
- return;
17597
- }
17598
- const store = ctx.getSessionStore();
17599
- await store.delete(id);
17710
+ await deleteWebUISession(
17711
+ {
17712
+ getActiveSessionId: () => ctx.getSession().id,
17713
+ getSessionStore: ctx.getSessionStore,
17714
+ refreshSessions: async () => {
17715
+ const list = await ctx.getSessionStore().list(200);
17716
+ broadcastToAll({
17717
+ type: "sessions.list",
17718
+ payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
17719
+ });
17720
+ }
17721
+ },
17722
+ id
17723
+ );
17600
17724
  result(ws, true, `Session ${id} deleted`);
17601
- try {
17602
- const list = await store.list(200);
17603
- broadcastToAll({
17604
- type: "sessions.list",
17605
- payload: { sessions: toSessionHistoryEntries(list, ctx.getSession().id) }
17606
- });
17607
- } catch {
17608
- }
17609
17725
  } catch (err) {
17610
17726
  result(ws, false, errMessage(err));
17611
17727
  }
@@ -21259,7 +21375,7 @@ import {
21259
21375
  expectDefined as expectDefined3,
21260
21376
  sessionScopedPath as sessionScopedPath3,
21261
21377
  startSharedHeapWatchdog,
21262
- toErrorMessage as toErrorMessage13,
21378
+ toErrorMessage as toErrorMessage14,
21263
21379
  wstackGlobalRoot as wstackGlobalRoot2
21264
21380
  } from "@wrongstack/core/utils";
21265
21381
  import { makeProviderFromConfig as makeProviderFromConfig3 } from "@wrongstack/providers";
@@ -21296,7 +21412,7 @@ import {
21296
21412
  } from "@wrongstack/core/types";
21297
21413
  import {
21298
21414
  estimateRequestTokensCalibrated,
21299
- toErrorMessage as toErrorMessage9
21415
+ toErrorMessage as toErrorMessage10
21300
21416
  } from "@wrongstack/core/utils";
21301
21417
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
21302
21418
  import { getSageService, setupSage } from "@wrongstack/sage";
@@ -21452,7 +21568,7 @@ function sleep(ms) {
21452
21568
  import { spawn as spawnChild } from "node:child_process";
21453
21569
  import { createRequire as createRequire2 } from "node:module";
21454
21570
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary2 } from "@wrongstack/core/security";
21455
- import { buildChildEnv, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
21571
+ import { buildChildEnv, toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
21456
21572
  var MAX_SESSIONS_PER_CLIENT = 8;
21457
21573
  var MAX_INPUT_BYTES = 1 << 16;
21458
21574
  var DEFAULT_COLS = 80;
@@ -21568,8 +21684,8 @@ var TerminalWebSocketHandler = class {
21568
21684
  ...windowsPtyOptions()
21569
21685
  });
21570
21686
  } catch (err) {
21571
- const msg = `Integrated terminal failed to start: ${toErrorMessage7(err)}`;
21572
- this.logger.warn?.(`terminal spawn failed: ${toErrorMessage7(err)}`);
21687
+ const msg = `Integrated terminal failed to start: ${toErrorMessage8(err)}`;
21688
+ this.logger.warn?.(`terminal spawn failed: ${toErrorMessage8(err)}`);
21573
21689
  this.send(ws, { type: "terminal.output", payload: { id: payload.id, data: `${msg}\r
21574
21690
  ` } });
21575
21691
  this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
@@ -21630,7 +21746,7 @@ var TerminalWebSocketHandler = class {
21630
21746
  try {
21631
21747
  pty.kill();
21632
21748
  } catch (err) {
21633
- this.logger.warn?.(`${reason} failed: ${toErrorMessage7(err)}`);
21749
+ this.logger.warn?.(`${reason} failed: ${toErrorMessage8(err)}`);
21634
21750
  }
21635
21751
  }
21636
21752
  killWindowsProcessTree(pty, reason) {
@@ -21642,7 +21758,7 @@ var TerminalWebSocketHandler = class {
21642
21758
  } catch {
21643
21759
  }
21644
21760
  } catch (err) {
21645
- this.logger.warn?.(`${reason} taskkill failed: ${toErrorMessage7(err)}`);
21761
+ this.logger.warn?.(`${reason} taskkill failed: ${toErrorMessage8(err)}`);
21646
21762
  }
21647
21763
  }
21648
21764
  send(ws, msg) {
@@ -21689,7 +21805,7 @@ function clampDim(value, fallback) {
21689
21805
 
21690
21806
  // src/server/worktree-ws-handler.ts
21691
21807
  import { join as join11, resolve as resolve13, sep as sep5 } from "node:path";
21692
- import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
21808
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
21693
21809
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
21694
21810
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
21695
21811
  var MAX_ACTIVITY = 6;
@@ -21855,7 +21971,7 @@ var WorktreeWebSocketHandler = class {
21855
21971
  }
21856
21972
  });
21857
21973
  } catch (err) {
21858
- this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage8(err)}`);
21974
+ this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage9(err)}`);
21859
21975
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
21860
21976
  }
21861
21977
  }
@@ -22117,7 +22233,7 @@ var WorktreeWebSocketHandler = class {
22117
22233
  try {
22118
22234
  sendSerialized(ws, data);
22119
22235
  } catch (err) {
22120
- this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage8(err)}`);
22236
+ this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage9(err)}`);
22121
22237
  }
22122
22238
  }
22123
22239
  }
@@ -22226,7 +22342,7 @@ async function createAgentServices(input) {
22226
22342
  const updateAutoCompactionMaxContext = async (newProvider, providerId = newProvider.id, providerCfg) => {
22227
22343
  await modelsRegistry.refresh().catch((err) => {
22228
22344
  logger.warn(
22229
- `models.dev refresh failed for ${providerId}/${context.model}: ${toErrorMessage9(err)}; using cached catalog`
22345
+ `models.dev refresh failed for ${providerId}/${context.model}: ${toErrorMessage10(err)}; using cached catalog`
22230
22346
  );
22231
22347
  });
22232
22348
  const currentConfig = input.config;
@@ -23068,7 +23184,7 @@ import {
23068
23184
  import {
23069
23185
  configureChildEnvGitIdentity,
23070
23186
  sessionScopedPath as sessionScopedPath2,
23071
- toErrorMessage as toErrorMessage11
23187
+ toErrorMessage as toErrorMessage12
23072
23188
  } from "@wrongstack/core/utils";
23073
23189
  import {
23074
23190
  createVaultBackedMcpAuthorizationProviderFactory,
@@ -23150,7 +23266,7 @@ async function discoverAndMergeWebuiProviders(opts) {
23150
23266
 
23151
23267
  // src/server/setup-screen.ts
23152
23268
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
23153
- import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
23269
+ import { toErrorMessage as toErrorMessage11 } from "@wrongstack/core/utils";
23154
23270
  import { makeProviderFromConfig } from "@wrongstack/providers";
23155
23271
  var UNCONFIGURED_CAPABILITIES = {
23156
23272
  tools: false,
@@ -23191,7 +23307,7 @@ function logCreateFailure(event, err) {
23191
23307
  JSON.stringify({
23192
23308
  level: "error",
23193
23309
  event,
23194
- message: toErrorMessage10(err),
23310
+ message: toErrorMessage11(err),
23195
23311
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
23196
23312
  })
23197
23313
  );
@@ -23500,7 +23616,7 @@ async function createPreContextServices(input) {
23500
23616
  await modelsRegistry.refresh();
23501
23617
  logger.info("models.dev catalog refreshed");
23502
23618
  } catch (err) {
23503
- logger.warn(`models.dev refresh failed (${toErrorMessage11(err)}); using cached catalog`);
23619
+ logger.warn(`models.dev refresh failed (${toErrorMessage12(err)}); using cached catalog`);
23504
23620
  }
23505
23621
  }
23506
23622
  try {
@@ -23511,7 +23627,7 @@ async function createPreContextServices(input) {
23511
23627
  logger
23512
23628
  });
23513
23629
  } catch (err) {
23514
- logger.debug(`provider auto-discovery skipped: ${toErrorMessage11(err)}`);
23630
+ logger.debug(`provider auto-discovery skipped: ${toErrorMessage12(err)}`);
23515
23631
  }
23516
23632
  try {
23517
23633
  await installCatalogModelOutputLimits({
@@ -23520,7 +23636,7 @@ async function createPreContextServices(input) {
23520
23636
  log: (message) => logger.debug(message)
23521
23637
  });
23522
23638
  } catch (err) {
23523
- logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
23639
+ logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
23524
23640
  }
23525
23641
  const events = opts.services?.events ?? new EventBus();
23526
23642
  events.setLogger(logger);
@@ -23539,7 +23655,7 @@ async function createPreContextServices(input) {
23539
23655
  JSON.stringify({
23540
23656
  level: "warn",
23541
23657
  event: "webui.provider_registry_load_failed",
23542
- message: toErrorMessage11(err),
23658
+ message: toErrorMessage12(err),
23543
23659
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
23544
23660
  })
23545
23661
  );
@@ -24174,7 +24290,7 @@ function buildRoutes(state, deps2, cb) {
24174
24290
  import { createRequire as createRequire4 } from "node:module";
24175
24291
  import * as path28 from "node:path";
24176
24292
  import { fileURLToPath } from "node:url";
24177
- import { toErrorMessage as toErrorMessage12 } from "@wrongstack/core/utils";
24293
+ import { toErrorMessage as toErrorMessage13 } from "@wrongstack/core/utils";
24178
24294
  import { WebSocketServer } from "ws";
24179
24295
  async function resolvePorts(opts) {
24180
24296
  const surface = opts.surface ?? "webui";
@@ -24325,7 +24441,7 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
24325
24441
  level: "error",
24326
24442
  event: "webui.ws_server_error",
24327
24443
  host: wsHost,
24328
- message: toErrorMessage12(err),
24444
+ message: toErrorMessage13(err),
24329
24445
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
24330
24446
  })
24331
24447
  );
@@ -24972,7 +25088,7 @@ async function startWebUI(opts = {}) {
24972
25088
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
24973
25089
  } catch (err) {
24974
25090
  console.warn(
24975
- `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage13(err)}`
25091
+ `[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage14(err)}`
24976
25092
  );
24977
25093
  }
24978
25094
  },
@@ -24995,6 +25111,19 @@ async function startWebUI(opts = {}) {
24995
25111
  })
24996
25112
  });
24997
25113
  const routes = buildRoutes(state, deps2, cb);
25114
+ const stopEmptySessionCleanup = scheduleOwnerlessEmptySessionCleanup({
25115
+ getSessionStore: state.getSessionStore,
25116
+ getActiveSessionId: () => state.getSession().id,
25117
+ hasParticipants: (sessionId) => collabHandler.hasParticipants(sessionId),
25118
+ refreshSessions: async () => {
25119
+ const list = await state.getSessionStore().list(200);
25120
+ broadcast(clients, {
25121
+ type: "sessions.list",
25122
+ payload: { sessions: toSessionHistoryEntries(list, state.getSession().id) }
25123
+ });
25124
+ },
25125
+ logger
25126
+ });
24998
25127
  let kanbanSupervisorDispose = null;
24999
25128
  const handleMessage = createMessageDispatcher({
25000
25129
  state,
@@ -25079,7 +25208,8 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
25079
25208
  wssPrimary,
25080
25209
  ...wssSecondary ? [wssSecondary] : []
25081
25210
  ],
25082
- onPreShutdown: () => {
25211
+ onPreShutdown: async () => {
25212
+ await stopEmptySessionCleanup.dispose();
25083
25213
  kanbanSupervisorDispose?.();
25084
25214
  kanbanSupervisorDispose = null;
25085
25215
  },
@@ -25106,9 +25236,9 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
25106
25236
  eternalSubscription = null;
25107
25237
  }
25108
25238
  codebaseIndexing.dispose();
25109
- await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage13(err)}`));
25239
+ await agentServices.runSageSessionHygiene().catch((err) => logger.warn(`sage session hygiene failed: ${toErrorMessage14(err)}`));
25110
25240
  await memoryStore.dispose().catch(
25111
- (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
25241
+ (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
25112
25242
  );
25113
25243
  await unregisterInstance(process.pid, path29.dirname(globalConfigPath));
25114
25244
  }
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export { type AutonomyRouteHandlers, createAutonomyRouteHandlers, handleAutonomyRoute, } from './autonomy-routes.js';
12
12
  export { bootConfig, patchConfig } from './boot.js';
13
+ export { cleanupOwnerlessEmptySessions, DEFAULT_EMPTY_SESSION_CLEANUP_INTERVAL_MS, EMPTY_SESSION_CLEANUP_INTERVAL_ENV, resolveEmptySessionCleanupInterval, scheduleOwnerlessEmptySessionCleanup, } from './session-cleanup-scheduler.js';
14
+ export { deleteWebUISession } from './session-deletion.js';
13
15
  export type { BrainRouteHandlers } from './brain-routes.js';
14
16
  export { handleBrainRoute } from './brain-routes.js';
15
17
  export { type ChronicleRouteContext, handleChronicleRoute, } from './chronicle-routes.js';
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { SecretVault } from '@wrongstack/core/types';
19
19
  /** Pref keys exposed to the settings panel via prefs.get / prefs.updated. */
20
- export declare const PREF_KEYS: readonly ['autonomy', 'autonomyDelayMs', 'autoProceedMaxIterations', 'yolo', 'maxIterations', 'chime', 'confirmExit', 'nextPrediction', 'nextStepsTool', 'enhanceEnabled', 'enhanceDelayMs', 'enhanceLanguage', 'featureMcp', 'featurePlugins', 'featureMemory', 'featureSkills', 'featureModelsRegistry', 'indexOnStart', 'contextAutoCompact', 'contextStrategy', 'contextMode', 'tokenSavingTier', 'maxConcurrent', 'titleAnimation', 'uiLocale', 'logLevel', 'auditLevel', 'hqEnabled', 'hqUrl', 'hqToken', 'hqRawContent', 'tgConfigured', 'tgSessionEnd', 'tgDelegate', 'tgLongToolMs', 'reasoningMode', 'reasoningEffort', 'reasoningPreserve', 'cacheTtl', 'fallbackModels', 'fallbackProfiles', 'favoriteModels', 'favoriteModelsOnly', 'modelAvailabilitySchedule', 'modelMatrix', 'fallbackAuto', 'refinerProvider', 'refinerModel', 'refinerFallbackProfile', 'thinkingWord', 'statuslineMode', 'animationStyle', 'showModelReasoning', 'breakerEnabled', 'breakerAutoKillResetMs', 'fsAccess', 'debugStream', 'chimeraEnabled', 'chimeraProvider', 'chimeraModel', 'chimeraMaxFiles', 'chimeraAutoFix', 'autoReviewEnabled', 'autoReviewProvider', 'autoReviewModel', 'autoReviewFallbackProfile', 'autoReviewModelSelection', 'autoReviewFallbackModels', 'autoReviewDebounceMs', 'autoReviewMaxFilesPerBatch', 'autoReviewMaxConcurrentReviews', 'autoReviewCascadeOn', 'groupToolCalls', 'showThinkingLogs', 'autoCollapseInput', 'pluginsEnabled', 'fleetChatVerbosity'];
20
+ export declare const PREF_KEYS: readonly ['autonomy', 'autonomyDelayMs', 'autoProceedMaxIterations', 'yolo', 'maxIterations', 'chime', 'confirmExit', 'nextPrediction', 'nextStepsTool', 'enhanceEnabled', 'enhanceDelayMs', 'enhanceLanguage', 'featureMcp', 'featurePlugins', 'featureMemory', 'featureSkills', 'featureModelsRegistry', 'indexOnStart', 'contextAutoCompact', 'contextStrategy', 'contextMode', 'tokenSavingTier', 'maxConcurrent', 'titleAnimation', 'uiLocale', 'logLevel', 'auditLevel', 'hqEnabled', 'hqUrl', 'hqRawContent', 'tgConfigured', 'tgSessionEnd', 'tgDelegate', 'tgLongToolMs', 'reasoningMode', 'reasoningEffort', 'reasoningPreserve', 'cacheTtl', 'fallbackModels', 'fallbackProfiles', 'favoriteModels', 'favoriteModelsOnly', 'modelAvailabilitySchedule', 'modelMatrix', 'fallbackAuto', 'refinerProvider', 'refinerModel', 'refinerFallbackProfile', 'thinkingWord', 'statuslineMode', 'animationStyle', 'showModelReasoning', 'breakerEnabled', 'breakerAutoKillResetMs', 'fsAccess', 'debugStream', 'chimeraEnabled', 'chimeraProvider', 'chimeraModel', 'chimeraMaxFiles', 'chimeraAutoFix', 'autoReviewEnabled', 'autoReviewProvider', 'autoReviewModel', 'autoReviewFallbackProfile', 'autoReviewModelSelection', 'autoReviewFallbackModels', 'autoReviewDebounceMs', 'autoReviewMaxFilesPerBatch', 'autoReviewMaxConcurrentReviews', 'autoReviewCascadeOn', 'groupToolCalls', 'showThinkingLogs', 'autoCollapseInput', 'pluginsEnabled', 'fleetChatVerbosity'];
21
21
  export interface PrefHelperDeps {
22
22
  /** Path to the active profile config; the sole settings mutation target. */
23
23
  profileConfigPath: string;
@@ -14,7 +14,7 @@ import type { Config, ModelsRegistry } from '@wrongstack/core/types';
14
14
  import { type WebSocket, WebSocketServer } from 'ws';
15
15
  import { type FileWatcherMetrics, setupEvents } from './setup-events.js';
16
16
  import type { ConnectedClient } from './types.js';
17
- interface ResolvedPorts {
17
+ export interface ResolvedPorts {
18
18
  wsHost: string;
19
19
  httpPort: number;
20
20
  publicUrl: string | undefined;