@wrongstack/webui-server 0.308.0 → 0.308.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7290,11 +7290,11 @@ async function handleGitDiff(ws, projectRoot, path34) {
7290
7290
  try {
7291
7291
  const git = makeGit(cwd);
7292
7292
  const { readFile: readFile11 } = await import("node:fs/promises");
7293
- const { join: join15 } = await import("node:path");
7293
+ const { join: join16 } = await import("node:path");
7294
7294
  const oldText = await git(["show", `HEAD:${path34}`]);
7295
7295
  let newText = "";
7296
7296
  try {
7297
- const abs = cwd ? join15(cwd, path34) : path34;
7297
+ const abs = cwd ? join16(cwd, path34) : path34;
7298
7298
  const buf = await readFile11(abs);
7299
7299
  if (buf.includes(0)) {
7300
7300
  reply2({ oldText: "", newText: "", binary: true });
@@ -10317,6 +10317,7 @@ function strictDecodeParam(segment, res) {
10317
10317
  }
10318
10318
 
10319
10319
  // src/server/http-server/vector-memory-handlers.ts
10320
+ import { getSageSurface } from "@wrongstack/sage";
10320
10321
  import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
10321
10322
  function snapshotVectorMemory(store, opts = {}) {
10322
10323
  const stats = store.stats();
@@ -10326,6 +10327,9 @@ function snapshotVectorMemory(store, opts = {}) {
10326
10327
  stats
10327
10328
  };
10328
10329
  }
10330
+ function snapshotVectorMemoryCache(store) {
10331
+ return store.cacheStats();
10332
+ }
10329
10333
  async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10330
10334
  const store = getStore();
10331
10335
  if (!store) {
@@ -10345,7 +10349,8 @@ async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10345
10349
  dimensions: snap.stats.dimensions,
10346
10350
  entries: snap.stats.entries,
10347
10351
  vectors: snap.stats.vectors,
10348
- providers: snap.stats.providers
10352
+ providers: snap.stats.providers,
10353
+ cache: snapshotVectorMemoryCache(store)
10349
10354
  };
10350
10355
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10351
10356
  res.end(JSON.stringify(body));
@@ -10365,7 +10370,29 @@ function parseSearchParams(url) {
10365
10370
  const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10));
10366
10371
  const rawThreshold = url.searchParams.get("threshold");
10367
10372
  const threshold = rawThreshold !== null && rawThreshold !== "" ? Math.max(0, Math.min(1, Number.parseFloat(rawThreshold))) : void 0;
10368
- return { query, limit, threshold: Number.isFinite(threshold) ? threshold : void 0 };
10373
+ const similarity = url.searchParams.get("similarity") === "1";
10374
+ return { query, limit, threshold: Number.isFinite(threshold) ? threshold : void 0, similarity };
10375
+ }
10376
+ function cosineMatrix(vectors) {
10377
+ const n = vectors.length;
10378
+ const out = new Array(n);
10379
+ for (let i = 0; i < n; i++) {
10380
+ out[i] = new Array(n).fill(0);
10381
+ }
10382
+ for (let i = 0; i < n; i++) {
10383
+ out[i][i] = 1;
10384
+ for (let j = i + 1; j < n; j++) {
10385
+ const a = vectors[i];
10386
+ const b = vectors[j];
10387
+ let dot = 0;
10388
+ const len = Math.min(a.length, b.length);
10389
+ for (let k = 0; k < len; k++) dot += (a[k] ?? 0) * (b[k] ?? 0);
10390
+ const score = Math.max(0, Math.min(1, dot));
10391
+ out[i][j] = score;
10392
+ out[j][i] = score;
10393
+ }
10394
+ }
10395
+ return out;
10369
10396
  }
10370
10397
  async function handleVectorMemorySearch(res, url, getStore) {
10371
10398
  const store = getStore();
@@ -10374,7 +10401,7 @@ async function handleVectorMemorySearch(res, url, getStore) {
10374
10401
  res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10375
10402
  return;
10376
10403
  }
10377
- const { query, limit, threshold } = parseSearchParams(url);
10404
+ const { query, limit, threshold, similarity } = parseSearchParams(url);
10378
10405
  if (query.trim().length === 0) {
10379
10406
  res.writeHead(400, { "Content-Type": "application/json" });
10380
10407
  res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
@@ -10383,7 +10410,8 @@ async function handleVectorMemorySearch(res, url, getStore) {
10383
10410
  try {
10384
10411
  const hits = await store.search(query, {
10385
10412
  limit,
10386
- ...threshold !== void 0 ? { threshold } : {}
10413
+ ...threshold !== void 0 ? { threshold } : {},
10414
+ includeVectors: similarity
10387
10415
  });
10388
10416
  const body = {
10389
10417
  hits: hits.map((h) => ({
@@ -10395,6 +10423,12 @@ async function handleVectorMemorySearch(res, url, getStore) {
10395
10423
  })),
10396
10424
  count: hits.length
10397
10425
  };
10426
+ if (similarity && hits.length > 1) {
10427
+ const vecs = hits.map((h) => h.vector).filter((v) => v !== void 0);
10428
+ if (vecs.length === hits.length) {
10429
+ body.similarity = cosineMatrix(vecs);
10430
+ }
10431
+ }
10398
10432
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10399
10433
  res.end(JSON.stringify(body));
10400
10434
  } catch (error2) {
@@ -10479,7 +10513,7 @@ async function handleVectorMemoryForget(res, url, getStore) {
10479
10513
  const id = match ? strictDecodeParam(decodeSessionId(match[1]), res) : null;
10480
10514
  if (id === null) return;
10481
10515
  try {
10482
- const removed = store.forget(id);
10516
+ const removed = await store.forget(id);
10483
10517
  res.writeHead(200, { "Content-Type": "application/json" });
10484
10518
  res.end(JSON.stringify({ removed }));
10485
10519
  } catch (error2) {
@@ -10492,6 +10526,86 @@ async function handleVectorMemoryForget(res, url, getStore) {
10492
10526
  );
10493
10527
  }
10494
10528
  }
10529
+ function parseMemorySearchParams(url) {
10530
+ const query = url.searchParams.get("q") ?? "";
10531
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10);
10532
+ const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 20));
10533
+ const explain = url.searchParams.get("explain") === "1";
10534
+ return { query, limit, explain };
10535
+ }
10536
+ async function handleMemorySearch(res, url, getStore) {
10537
+ const store = getStore();
10538
+ if (!store) {
10539
+ res.writeHead(503, { "Content-Type": "application/json" });
10540
+ res.end(JSON.stringify({ error: "Memory store not enabled in this host" }));
10541
+ return;
10542
+ }
10543
+ const { query, limit, explain } = parseMemorySearchParams(url);
10544
+ if (query.trim().length === 0) {
10545
+ res.writeHead(400, { "Content-Type": "application/json" });
10546
+ res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
10547
+ return;
10548
+ }
10549
+ const Sage = getSageSurface(store);
10550
+ if (!Sage) {
10551
+ res.writeHead(503, { "Content-Type": "application/json" });
10552
+ res.end(
10553
+ JSON.stringify({
10554
+ error: "Memory search requires the SAGE surface (this host does not expose it)."
10555
+ })
10556
+ );
10557
+ return;
10558
+ }
10559
+ try {
10560
+ let payload;
10561
+ if (explain && typeof Sage.searchSageWithBreakdown === "function") {
10562
+ const hits = await Sage.searchSageWithBreakdown(query, { limit });
10563
+ payload = {
10564
+ count: hits.length,
10565
+ channel: "breakdown",
10566
+ hits: hits.map((h) => ({
10567
+ id: h.memory.id,
10568
+ text: h.memory.text,
10569
+ kind: h.memory.kind,
10570
+ status: h.memory.status,
10571
+ tags: h.memory.tags ?? [],
10572
+ lexicalScore: h.lexicalScore,
10573
+ vectorScore: h.vectorScore,
10574
+ finalScore: h.finalScore,
10575
+ source: h.source
10576
+ }))
10577
+ };
10578
+ } else {
10579
+ const rows = await Sage.searchSage(query, { limit });
10580
+ const total = rows.length;
10581
+ payload = {
10582
+ count: total,
10583
+ channel: "lexical",
10584
+ hits: rows.map((memory, index) => ({
10585
+ id: memory.id,
10586
+ text: memory.text,
10587
+ kind: memory.kind,
10588
+ status: memory.status,
10589
+ tags: memory.tags ?? [],
10590
+ lexicalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
10591
+ vectorScore: null,
10592
+ finalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
10593
+ source: "lexical"
10594
+ }))
10595
+ };
10596
+ }
10597
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10598
+ res.end(JSON.stringify(payload));
10599
+ } catch (error2) {
10600
+ res.writeHead(500, { "Content-Type": "application/json" });
10601
+ res.end(
10602
+ JSON.stringify({
10603
+ error: "Memory search failed",
10604
+ detail: sanitizeApiError2(error2)
10605
+ })
10606
+ );
10607
+ }
10608
+ }
10495
10609
 
10496
10610
  // src/server/http-server/api-router.ts
10497
10611
  async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessTokenOk, getTechStackRuntime) {
@@ -10942,6 +11056,15 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
10942
11056
  await handleVectorMemoryStore(res, req, () => deps2.getVectorMemoryStore?.());
10943
11057
  return true;
10944
11058
  }
11059
+ if (url.pathname === "/api/memory/search" && req.method === "GET") {
11060
+ if (requireAccessToken && !accessTokenOk) {
11061
+ res.writeHead(401, { "Content-Type": "application/json" });
11062
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11063
+ return true;
11064
+ }
11065
+ await handleMemorySearch(res, url, () => deps2.getMemoryStore?.());
11066
+ return true;
11067
+ }
10945
11068
  if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
10946
11069
  await handleDeadCodeActionPlan(
10947
11070
  res,
@@ -11106,12 +11229,15 @@ function createHttpServer(opts) {
11106
11229
  distDir,
11107
11230
  url,
11108
11231
  opts,
11109
- port,
11232
+ // Live port from the socket: the bind may have advanced past an
11233
+ // EADDRINUSE (listenWithRetry) after this server was constructed,
11234
+ // and the CSP must advertise the port actually serving this request.
11235
+ res.socket?.localPort ?? port,
11110
11236
  shouldSetAuthCookie
11111
11237
  );
11112
11238
  } catch (err) {
11113
11239
  if (err.code === "ENOENT") {
11114
- await handleSpaFallback(res, distDir, opts, port);
11240
+ await handleSpaFallback(res, distDir, opts, res.socket?.localPort ?? port);
11115
11241
  } else {
11116
11242
  console.error({ url: req.url, err });
11117
11243
  res.writeHead(500);
@@ -13896,13 +14022,13 @@ async function handleMcpRoute(ws, msg, handlers) {
13896
14022
  }
13897
14023
 
13898
14024
  // src/server/memory-handlers.ts
13899
- import { getSageSurface } from "@wrongstack/sage";
14025
+ import { getSageSurface as getSageSurface2 } from "@wrongstack/sage";
13900
14026
  function requiresSage(command) {
13901
14027
  return `\`${command}\` requires the SAGE backend (Sage.enabled).`;
13902
14028
  }
13903
14029
  async function handleMemoryList(ws, memoryStore) {
13904
14030
  try {
13905
- const Sage = getSageSurface(memoryStore);
14031
+ const Sage = getSageSurface2(memoryStore);
13906
14032
  if (Sage) {
13907
14033
  const [stats, memories] = await Promise.all([Sage.stats(), Sage.listSage()]);
13908
14034
  const text2 = memories.length === 0 ? "\u{1F9E0} SAGE is empty." : formatSageText(stats, memories);
@@ -13937,7 +14063,7 @@ function formatSageText(stats, memories) {
13937
14063
  return lines.join("\n");
13938
14064
  }
13939
14065
  async function handleSageList(ws, memoryStore) {
13940
- const Sage = getSageSurface(memoryStore);
14066
+ const Sage = getSageSurface2(memoryStore);
13941
14067
  if (!Sage) {
13942
14068
  send(ws, {
13943
14069
  type: "memory.sage.list",
@@ -13953,7 +14079,7 @@ async function handleSageList(ws, memoryStore) {
13953
14079
  }
13954
14080
  }
13955
14081
  async function handleSageListPage(ws, msg, memoryStore) {
13956
- const Sage = getSageSurface(memoryStore);
14082
+ const Sage = getSageSurface2(memoryStore);
13957
14083
  if (!Sage) {
13958
14084
  send(ws, {
13959
14085
  type: "memory.sage.listPage",
@@ -14003,8 +14129,50 @@ async function handleSageListPage(ws, msg, memoryStore) {
14003
14129
  send(ws, { type: "memory.sage.listPage", payload: { error: errMessage(err) } });
14004
14130
  }
14005
14131
  }
14132
+ async function handleSageSearchBreakdown(ws, msg, memoryStore) {
14133
+ const Sage = getSageSurface2(memoryStore);
14134
+ if (!Sage) {
14135
+ send(ws, {
14136
+ type: "memory.sage.searchBreakdown",
14137
+ payload: { error: requiresSage("memory.sage.searchBreakdown") }
14138
+ });
14139
+ return;
14140
+ }
14141
+ try {
14142
+ const payload = msg.payload ?? {};
14143
+ const query = typeof payload["query"] === "string" ? payload["query"] : "";
14144
+ if (query.trim().length === 0) {
14145
+ send(ws, {
14146
+ type: "memory.sage.searchBreakdown",
14147
+ payload: { error: "Missing required field `query`" }
14148
+ });
14149
+ return;
14150
+ }
14151
+ const limit = typeof payload["limit"] === "number" ? payload["limit"] : 20;
14152
+ const includeStale = payload["includeStale"] === true;
14153
+ const includeStatuses = includeStale ? ["active", "stale"] : ["active"];
14154
+ const options = { limit, includeStatuses };
14155
+ if (typeof Sage.searchSageWithBreakdown === "function") {
14156
+ const hits2 = await Sage.searchSageWithBreakdown(query, options);
14157
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { hits: hits2, source: "breakdown" } });
14158
+ return;
14159
+ }
14160
+ const rows = await Sage.searchSage(query, options);
14161
+ const total = rows.length;
14162
+ const hits = rows.map((memory, index) => ({
14163
+ memory,
14164
+ vectorScore: null,
14165
+ lexicalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
14166
+ finalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
14167
+ source: "lexical"
14168
+ }));
14169
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { hits, source: "lexical" } });
14170
+ } catch (err) {
14171
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { error: errMessage(err) } });
14172
+ }
14173
+ }
14006
14174
  async function handleSageGet(ws, msg, memoryStore) {
14007
- const Sage = getSageSurface(memoryStore);
14175
+ const Sage = getSageSurface2(memoryStore);
14008
14176
  if (!Sage) {
14009
14177
  send(ws, {
14010
14178
  type: "memory.sage.get",
@@ -14029,7 +14197,7 @@ async function handleSageGet(ws, msg, memoryStore) {
14029
14197
  }
14030
14198
  }
14031
14199
  async function handleSageGraph(ws, msg, memoryStore) {
14032
- const Sage = getSageSurface(memoryStore);
14200
+ const Sage = getSageSurface2(memoryStore);
14033
14201
  if (!Sage?.graphFor) {
14034
14202
  send(ws, {
14035
14203
  type: "memory.sage.graph",
@@ -14063,7 +14231,7 @@ async function handleSageGraph(ws, msg, memoryStore) {
14063
14231
  }
14064
14232
  }
14065
14233
  async function handleSageUpdate(ws, msg, memoryStore) {
14066
- const Sage = getSageSurface(memoryStore);
14234
+ const Sage = getSageSurface2(memoryStore);
14067
14235
  if (!Sage) {
14068
14236
  send(ws, {
14069
14237
  type: "memory.sage.update",
@@ -14091,7 +14259,7 @@ async function handleSageUpdate(ws, msg, memoryStore) {
14091
14259
  }
14092
14260
  }
14093
14261
  async function handleSageRemember(ws, msg, memoryStore) {
14094
- const Sage = getSageSurface(memoryStore);
14262
+ const Sage = getSageSurface2(memoryStore);
14095
14263
  if (!Sage) {
14096
14264
  send(ws, {
14097
14265
  type: "memory.sage.remember",
@@ -14125,7 +14293,7 @@ async function handleSageRemember(ws, msg, memoryStore) {
14125
14293
  }
14126
14294
  }
14127
14295
  async function handleSageDelete(ws, msg, memoryStore) {
14128
- const Sage = getSageSurface(memoryStore);
14296
+ const Sage = getSageSurface2(memoryStore);
14129
14297
  if (!Sage) {
14130
14298
  send(ws, {
14131
14299
  type: "memory.sage.delete",
@@ -14158,7 +14326,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
14158
14326
  }
14159
14327
  }
14160
14328
  async function handleSageRecover(ws, msg, memoryStore) {
14161
- const Sage = getSageSurface(memoryStore);
14329
+ const Sage = getSageSurface2(memoryStore);
14162
14330
  if (!Sage?.recoverSage) {
14163
14331
  send(ws, {
14164
14332
  type: "memory.sage.recover",
@@ -14202,7 +14370,7 @@ async function handleSageRecover(ws, msg, memoryStore) {
14202
14370
  }
14203
14371
  }
14204
14372
  async function handleSageListCandidates(ws, msg, memoryStore) {
14205
- const Sage = getSageSurface(memoryStore);
14373
+ const Sage = getSageSurface2(memoryStore);
14206
14374
  if (!Sage) {
14207
14375
  send(ws, {
14208
14376
  type: "memory.sage.listCandidates",
@@ -14230,7 +14398,7 @@ async function handleSageListCandidates(ws, msg, memoryStore) {
14230
14398
  }
14231
14399
  }
14232
14400
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
14233
- const Sage = getSageSurface(memoryStore);
14401
+ const Sage = getSageSurface2(memoryStore);
14234
14402
  if (!Sage) {
14235
14403
  send(ws, {
14236
14404
  type: "memory.sage.candidateResolve",
@@ -14287,7 +14455,7 @@ async function handleSageCandidateResolve(ws, msg, memoryStore) {
14287
14455
  }
14288
14456
  }
14289
14457
  async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14290
- const Sage = getSageSurface(memoryStore);
14458
+ const Sage = getSageSurface2(memoryStore);
14291
14459
  if (!Sage?.backfillRecoverable) {
14292
14460
  send(ws, {
14293
14461
  type: "memory.sage.backfillRecoverable",
@@ -14327,7 +14495,7 @@ async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14327
14495
  }
14328
14496
  }
14329
14497
  async function handleSageForFile(ws, msg, memoryStore) {
14330
- const Sage = getSageSurface(memoryStore);
14498
+ const Sage = getSageSurface2(memoryStore);
14331
14499
  if (!Sage?.findMemoriesForFile) {
14332
14500
  send(ws, {
14333
14501
  type: "memory.sage.forFile",
@@ -14419,6 +14587,9 @@ async function handleMemoryRoute(ctx, ws, message) {
14419
14587
  case "memory.sage.forFile":
14420
14588
  await handleSageForFile(ws, message, store);
14421
14589
  return true;
14590
+ case "memory.sage.searchBreakdown":
14591
+ await handleSageSearchBreakdown(ws, message, store);
14592
+ return true;
14422
14593
  default:
14423
14594
  return false;
14424
14595
  }
@@ -14762,17 +14933,25 @@ function createModelOperations(context) {
14762
14933
  // src/server/port-utils.ts
14763
14934
  import * as net2 from "node:net";
14764
14935
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
14936
+ var MAX_TCP_PORT = 65535;
14937
+ function isStrictPort() {
14938
+ const value = process.env["WEBUI_STRICT_PORT"];
14939
+ return value === "1" || value === "true";
14940
+ }
14765
14941
  function isPortFree(host, port) {
14942
+ return probePort(host, port).then((err) => err === null);
14943
+ }
14944
+ function probePort(host, port) {
14766
14945
  return new Promise((resolve19) => {
14767
14946
  const srv = net2.createServer();
14768
- srv.once("error", () => resolve19(false));
14947
+ srv.once("error", (err) => resolve19(err));
14769
14948
  srv.once("listening", () => {
14770
- srv.close(() => resolve19(true));
14949
+ srv.close(() => resolve19(null));
14771
14950
  });
14772
14951
  try {
14773
14952
  srv.listen(port, host);
14774
- } catch {
14775
- resolve19(false);
14953
+ } catch (err) {
14954
+ resolve19(err);
14776
14955
  }
14777
14956
  });
14778
14957
  }
@@ -14781,7 +14960,7 @@ async function findFreePort(host, startPort, opts = {}) {
14781
14960
  const maxTries = opts.maxTries ?? 200;
14782
14961
  let port = startPort;
14783
14962
  for (let i = 0; i < maxTries; i++) {
14784
- if (port > 65535) port = 1024 + port % 5e4;
14963
+ if (port > MAX_TCP_PORT) port = 1024 + port % 5e4;
14785
14964
  if (!exclude.has(port) && await isPortFree(host, port)) {
14786
14965
  return port;
14787
14966
  }
@@ -14792,6 +14971,50 @@ async function findFreePort(host, startPort, opts = {}) {
14792
14971
  field: "port"
14793
14972
  });
14794
14973
  }
14974
+ function listenWithRetry(server, host, port, opts = {}) {
14975
+ const maxTries = opts.maxTries ?? 10;
14976
+ return new Promise((resolve19, reject) => {
14977
+ const canAdvance = (candidate) => candidate < MAX_TCP_PORT;
14978
+ const probeable = (candidate) => Number.isInteger(candidate) && candidate >= 0 && candidate <= MAX_TCP_PORT;
14979
+ const attempt = (candidate, remaining) => {
14980
+ void (async () => {
14981
+ if (probeable(candidate)) {
14982
+ const probeErr = await probePort(host, candidate);
14983
+ if (probeErr !== null) {
14984
+ if (probeErr.code === "EADDRINUSE" && remaining > 1 && canAdvance(candidate)) {
14985
+ attempt(candidate + 1, remaining - 1);
14986
+ return;
14987
+ }
14988
+ reject(probeErr);
14989
+ return;
14990
+ }
14991
+ }
14992
+ const onError = (err) => {
14993
+ server.off("listening", onListening);
14994
+ server.off("error", onError);
14995
+ if (err.code === "EADDRINUSE" && remaining > 1 && canAdvance(candidate)) {
14996
+ attempt(candidate + 1, remaining - 1);
14997
+ return;
14998
+ }
14999
+ reject(err);
15000
+ };
15001
+ const onListening = () => {
15002
+ server.off("error", onError);
15003
+ const address = server.address();
15004
+ resolve19(address && typeof address === "object" ? address.port : candidate);
15005
+ };
15006
+ server.once("error", onError);
15007
+ server.once("listening", onListening);
15008
+ try {
15009
+ server.listen(candidate, host);
15010
+ } catch (err) {
15011
+ onError(err);
15012
+ }
15013
+ })();
15014
+ };
15015
+ attempt(port, maxTries);
15016
+ });
15017
+ }
14795
15018
 
14796
15019
  // src/server/intake-service.ts
14797
15020
  import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
@@ -14994,9 +15217,19 @@ async function handleBrainAsk(ctx, ws, question) {
14994
15217
  risk: "medium",
14995
15218
  fallback: "ask_human"
14996
15219
  });
15220
+ const answerSessionId = ctx.getSessionId?.();
14997
15221
  ctx.send(ws, {
14998
15222
  type: "brain.answer",
14999
- payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
15223
+ // Omit sessionId when there is no session: this is a direct reply to
15224
+ // the asker, not a broadcast, but the client's session gate
15225
+ // (isActiveSessionMessage) is fail-closed on a present-but-empty
15226
+ // sessionId — stamping '' would hide the answer from its own asker
15227
+ // in an embedded host with an unbound agent context.
15228
+ payload: {
15229
+ ...answerSessionId ? { sessionId: answerSessionId } : {},
15230
+ question: q,
15231
+ decision
15232
+ }
15000
15233
  });
15001
15234
  } catch (err) {
15002
15235
  sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
@@ -17080,7 +17313,12 @@ function createProviderHandlers(deps2) {
17080
17313
 
17081
17314
  // src/server/session-handlers.ts
17082
17315
  import { loadTodosCheckpoint } from "@wrongstack/core/storage";
17083
- import { DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveContextWindowPolicy } from "@wrongstack/core/types";
17316
+ import {
17317
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY,
17318
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
17319
+ isContextWindowModeId,
17320
+ resolveContextWindowPolicy
17321
+ } from "@wrongstack/core/types";
17084
17322
  import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
17085
17323
 
17086
17324
  // src/protocol/client-conversation.ts
@@ -17177,6 +17415,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
17177
17415
  "memory.sage.listPage",
17178
17416
  "memory.sage.recover",
17179
17417
  "memory.sage.remember",
17418
+ "memory.sage.searchBreakdown",
17180
17419
  "memory.sage.update"
17181
17420
  ];
17182
17421
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -17464,6 +17703,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
17464
17703
  "memory.sage.listPage",
17465
17704
  "memory.sage.recover",
17466
17705
  "memory.sage.remember",
17706
+ "memory.sage.searchBreakdown",
17467
17707
  "memory.sage.update"
17468
17708
  ];
17469
17709
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -17600,6 +17840,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
17600
17840
  var SERVER_CONFIGURATION_MESSAGE_TYPES = [
17601
17841
  "auth.oauth.status",
17602
17842
  "codebase.index.server.shutdown_result",
17843
+ "connections.auto_heal_status",
17603
17844
  "connections.health_error",
17604
17845
  "connections.health_result",
17605
17846
  "connections.service_action_result",
@@ -18150,7 +18391,12 @@ function createSessionHandlers(ctx) {
18150
18391
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
18151
18392
  );
18152
18393
  ctx.tokenCounter.reset?.();
18153
- if (usage) ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
18394
+ if (usage) {
18395
+ ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
18396
+ if (typeof usage.input === "number" && usage.input > 0) {
18397
+ ctx.context.lastRequestTokens = usage.input;
18398
+ }
18399
+ }
18154
18400
  ctx.setSessionStartedAt?.(Date.now());
18155
18401
  await ctx.onSessionSwapped?.(next.id);
18156
18402
  };
@@ -18373,8 +18619,8 @@ function createSessionHandlers(ctx) {
18373
18619
  return;
18374
18620
  }
18375
18621
  const { id } = parsed.value;
18376
- let policy = resolveContextWindowPolicy({}, id);
18377
- if (policy.id !== id) {
18622
+ let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
18623
+ if (!isContextWindowModeId(id) && policy.id !== id) {
18378
18624
  const customModes = (await modeStore()).list().filter((m) => m.custom === true);
18379
18625
  const custom = customModes.find((m) => m.id === id);
18380
18626
  if (!custom) {
@@ -18385,6 +18631,7 @@ function createSessionHandlers(ctx) {
18385
18631
  }
18386
18632
  ctx.context.meta["contextWindowMode"] = policy.id;
18387
18633
  ctx.context.meta["contextWindowPolicy"] = policy;
18634
+ ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
18388
18635
  result(ws, true, `Context mode switched to ${policy.id}`);
18389
18636
  broadcastToAll({
18390
18637
  type: "context.mode.changed",
@@ -18448,11 +18695,14 @@ function createSessionHandlers(ctx) {
18448
18695
  }
18449
18696
  const { id } = parsed.value;
18450
18697
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
18451
- ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
18452
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
18698
+ const policy = resolveContextWindowPolicy(
18453
18699
  {},
18454
- DEFAULT_CONTEXT_WINDOW_MODE_ID
18700
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
18701
+ readSessionWindowTokens(ctx.context)
18455
18702
  );
18703
+ ctx.context.meta["contextWindowMode"] = policy.id;
18704
+ ctx.context.meta["contextWindowPolicy"] = policy;
18705
+ delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
18456
18706
  }
18457
18707
  const store = await modeStore();
18458
18708
  const operation = store.remove(id);
@@ -18659,6 +18909,12 @@ function createSessionHandlers(ctx) {
18659
18909
  }
18660
18910
  };
18661
18911
  }
18912
+ function readSessionWindowTokens(context) {
18913
+ const meta = context.meta?.["effectiveMaxContext"];
18914
+ if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
18915
+ const cap = context.provider?.capabilities?.maxContext;
18916
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
18917
+ }
18662
18918
 
18663
18919
  // src/server/agent-roster-handlers.ts
18664
18920
  import {
@@ -19157,6 +19413,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
19157
19413
  return true;
19158
19414
  }
19159
19415
 
19416
+ // src/server/connections/auto-healer.ts
19417
+ var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
19418
+ var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
19419
+ var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
19420
+ var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
19421
+ var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
19422
+ "kanban",
19423
+ "sage",
19424
+ "chronicle",
19425
+ "codebase-index",
19426
+ "mailbox"
19427
+ ]);
19428
+ function isAutoHealEnabled() {
19429
+ return process.env[AUTO_HEAL_ENV_FLAG] === "1";
19430
+ }
19431
+ function createAutoHealer(options) {
19432
+ const enabled = options.enabled ?? isAutoHealEnabled();
19433
+ const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
19434
+ const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
19435
+ const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
19436
+ const collect = options.collect ?? (() => collectConnectionsHealth({
19437
+ projectRoot: options.projectRoot(),
19438
+ indexDir: options.indexDir(),
19439
+ backend: "standalone"
19440
+ }));
19441
+ const execute = options.execute ?? executeServiceAction;
19442
+ const services = /* @__PURE__ */ new Map();
19443
+ let timer = null;
19444
+ let running = false;
19445
+ let ticking = false;
19446
+ let disposed = false;
19447
+ let inFlightTick = null;
19448
+ let lastTickAt = null;
19449
+ let warnedNoBoundary = false;
19450
+ function stateFor(serviceId) {
19451
+ let state = services.get(serviceId);
19452
+ if (!state) {
19453
+ state = {
19454
+ lastAttemptAt: null,
19455
+ consecutiveFailures: 0,
19456
+ lastSuccess: null,
19457
+ lastMessage: null,
19458
+ inFlight: false,
19459
+ escalated: false
19460
+ };
19461
+ services.set(serviceId, state);
19462
+ }
19463
+ return state;
19464
+ }
19465
+ function snapshot() {
19466
+ return {
19467
+ enabled,
19468
+ running,
19469
+ lastTickAt,
19470
+ services: Object.fromEntries(services)
19471
+ };
19472
+ }
19473
+ function emitStatus(event) {
19474
+ try {
19475
+ options.onStatus?.({ ...event, at: Date.now() });
19476
+ } catch (error2) {
19477
+ options.logger?.warn?.(
19478
+ `[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
19479
+ );
19480
+ }
19481
+ }
19482
+ async function tick() {
19483
+ if (!enabled || disposed) return snapshot();
19484
+ if (!options.trustBoundary) {
19485
+ if (!warnedNoBoundary) {
19486
+ warnedNoBoundary = true;
19487
+ options.logger?.warn?.(
19488
+ "[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
19489
+ );
19490
+ }
19491
+ return snapshot();
19492
+ }
19493
+ if (ticking) return snapshot();
19494
+ ticking = true;
19495
+ try {
19496
+ const report = await collect();
19497
+ const now = Date.now();
19498
+ const projectRoot = options.projectRoot();
19499
+ const indexDir = options.indexDir();
19500
+ for (const service of report.services) {
19501
+ if (disposed) break;
19502
+ const state = stateFor(service.id);
19503
+ if (service.status !== "error") {
19504
+ state.consecutiveFailures = 0;
19505
+ state.escalated = false;
19506
+ continue;
19507
+ }
19508
+ if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
19509
+ continue;
19510
+ }
19511
+ if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
19512
+ if (state.consecutiveFailures >= maxAttempts) {
19513
+ state.escalated = true;
19514
+ options.logger?.warn?.(
19515
+ `[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
19516
+ );
19517
+ continue;
19518
+ }
19519
+ if (state.inFlight) continue;
19520
+ const authorization = await authorizeWebUIAction(
19521
+ options.trustBoundary,
19522
+ {
19523
+ capability: "connections.service.restart",
19524
+ subject: { kind: "process", id: `${service.id}@${projectRoot}` },
19525
+ risk: "elevated",
19526
+ cwd: projectRoot,
19527
+ metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
19528
+ },
19529
+ options.logger
19530
+ );
19531
+ if (disposed) break;
19532
+ if (!authorization.allowed) {
19533
+ state.lastAttemptAt = now;
19534
+ state.lastMessage = `refused by policy: ${authorization.reason}`;
19535
+ emitStatus({
19536
+ serviceId: service.id,
19537
+ phase: "refused",
19538
+ message: `refused by policy: ${authorization.reason}`,
19539
+ attempt: state.consecutiveFailures + 1
19540
+ });
19541
+ options.logger?.warn?.(
19542
+ `[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
19543
+ );
19544
+ continue;
19545
+ }
19546
+ state.inFlight = true;
19547
+ const attempt = state.consecutiveFailures + 1;
19548
+ emitStatus({
19549
+ serviceId: service.id,
19550
+ phase: "restarting",
19551
+ message: `Auto-restarting ${service.id}`,
19552
+ attempt
19553
+ });
19554
+ try {
19555
+ const result = await execute(service.id, "restart", projectRoot, indexDir);
19556
+ state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
19557
+ state.lastSuccess = result.success;
19558
+ state.lastMessage = result.message;
19559
+ emitStatus({
19560
+ serviceId: service.id,
19561
+ phase: result.success ? "restarted" : "failed",
19562
+ message: result.message,
19563
+ attempt
19564
+ });
19565
+ if (!result.success && state.consecutiveFailures >= maxAttempts) {
19566
+ state.escalated = true;
19567
+ emitStatus({
19568
+ serviceId: service.id,
19569
+ phase: "escalated",
19570
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
19571
+ attempt
19572
+ });
19573
+ options.logger?.warn?.(
19574
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
19575
+ );
19576
+ }
19577
+ options.logger?.[result.success ? "info" : "warn"]?.(
19578
+ `[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
19579
+ );
19580
+ } catch (error2) {
19581
+ state.consecutiveFailures += 1;
19582
+ state.lastSuccess = false;
19583
+ state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
19584
+ emitStatus({
19585
+ serviceId: service.id,
19586
+ phase: "failed",
19587
+ message: state.lastMessage,
19588
+ attempt
19589
+ });
19590
+ if (state.consecutiveFailures >= maxAttempts) {
19591
+ state.escalated = true;
19592
+ emitStatus({
19593
+ serviceId: service.id,
19594
+ phase: "escalated",
19595
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
19596
+ attempt
19597
+ });
19598
+ options.logger?.warn?.(
19599
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
19600
+ );
19601
+ }
19602
+ options.logger?.warn?.(
19603
+ `[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
19604
+ );
19605
+ } finally {
19606
+ state.lastAttemptAt = Date.now();
19607
+ state.inFlight = false;
19608
+ }
19609
+ }
19610
+ lastTickAt = Date.now();
19611
+ } catch (error2) {
19612
+ options.logger?.warn?.(
19613
+ `[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
19614
+ );
19615
+ } finally {
19616
+ ticking = false;
19617
+ }
19618
+ return snapshot();
19619
+ }
19620
+ function runTick() {
19621
+ if (!enabled || disposed || running === false || ticking) return;
19622
+ const pending = tick();
19623
+ const tracked = pending.then(
19624
+ () => void 0,
19625
+ () => void 0
19626
+ );
19627
+ inFlightTick = tracked;
19628
+ void tracked.finally(() => {
19629
+ if (inFlightTick === tracked) inFlightTick = null;
19630
+ });
19631
+ }
19632
+ function stopInternal() {
19633
+ if (timer) {
19634
+ clearInterval(timer);
19635
+ timer = null;
19636
+ }
19637
+ running = false;
19638
+ }
19639
+ return {
19640
+ start() {
19641
+ if (!enabled || running || disposed) return;
19642
+ running = true;
19643
+ runTick();
19644
+ timer = setInterval(runTick, intervalMs);
19645
+ timer.unref?.();
19646
+ },
19647
+ stop: stopInternal,
19648
+ async dispose() {
19649
+ stopInternal();
19650
+ disposed = true;
19651
+ const pending = inFlightTick;
19652
+ if (pending) {
19653
+ await Promise.race([
19654
+ pending,
19655
+ new Promise((resolve19) => {
19656
+ const t = setTimeout(resolve19, 3e4);
19657
+ t.unref?.();
19658
+ })
19659
+ ]);
19660
+ }
19661
+ disposed = true;
19662
+ },
19663
+ tick,
19664
+ getSnapshot: snapshot,
19665
+ isRunning: () => running
19666
+ };
19667
+ }
19668
+
19160
19669
  // src/server/fallback-choice.ts
19161
19670
  function emitFallbackChoice(events, msg) {
19162
19671
  const parsed = validateModelFallbackChoicePayload(msg.payload);
@@ -22310,6 +22819,11 @@ import {
22310
22819
  wstackGlobalRoot as wstackGlobalRoot2
22311
22820
  } from "@wrongstack/core/utils";
22312
22821
  import { ensureSessionShell } from "@wrongstack/tools";
22822
+ import {
22823
+ TransformersEmbeddingProvider,
22824
+ VectorMemoryStore,
22825
+ startFirstBootSageSync
22826
+ } from "@wrongstack/vector-memory";
22313
22827
 
22314
22828
  // src/server/backend-services.ts
22315
22829
  import { join as join12 } from "node:path";
@@ -22337,6 +22851,7 @@ import {
22337
22851
  import { TOKENS } from "@wrongstack/core/kernel";
22338
22852
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
22339
22853
  import {
22854
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
22340
22855
  DEFAULT_TOOLS_CONFIG,
22341
22856
  resolveContextWindowPolicy as resolveContextWindowPolicy2
22342
22857
  } from "@wrongstack/core/types";
@@ -23257,22 +23772,26 @@ async function createAgentServices(input) {
23257
23772
  summarizerModel: config.context?.summarizerModel,
23258
23773
  llmSelector: config.context?.llmSelector
23259
23774
  });
23260
- const initialContextPolicy = resolveContextWindowPolicy2(config.context);
23775
+ let effectiveMaxContext = 0;
23776
+ try {
23777
+ const m = await resolveProviderModelMetadata(
23778
+ modelsRegistry,
23779
+ config.provider,
23780
+ context.model,
23781
+ config.providers?.[config.provider]
23782
+ );
23783
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23784
+ } catch {
23785
+ }
23786
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23787
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23788
+ const initialContextPolicy = resolveContextWindowPolicy2(
23789
+ config.context,
23790
+ void 0,
23791
+ effectiveMaxContext
23792
+ );
23261
23793
  let autoCompactor;
23262
23794
  if (config.context?.autoCompact !== false) {
23263
- let effectiveMaxContext = 0;
23264
- try {
23265
- const m = await resolveProviderModelMetadata(
23266
- modelsRegistry,
23267
- config.provider,
23268
- context.model,
23269
- config.providers?.[config.provider]
23270
- );
23271
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23272
- } catch {
23273
- }
23274
- if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23275
- if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23276
23795
  autoCompactor = new AutoCompactionMiddlewareCtor(
23277
23796
  compactor,
23278
23797
  effectiveMaxContext,
@@ -23327,6 +23846,15 @@ async function createAgentServices(input) {
23327
23846
  context.meta["effectiveMaxContext"] = newMaxContext;
23328
23847
  autoCompactor?.setMaxContext(newMaxContext);
23329
23848
  autoCompactor?.setEnabled(config.context?.autoCompact !== false);
23849
+ if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
23850
+ const policy = resolveContextWindowPolicy2(
23851
+ currentConfig.context ?? {},
23852
+ void 0,
23853
+ newMaxContext
23854
+ );
23855
+ context.meta["contextWindowMode"] = policy.id;
23856
+ context.meta["contextWindowPolicy"] = policy;
23857
+ }
23330
23858
  } else {
23331
23859
  delete context.meta["effectiveMaxContext"];
23332
23860
  autoCompactor?.setEnabled(false);
@@ -23958,8 +24486,22 @@ function createMessageDispatcher(opts) {
23958
24486
  broadcast: (message) => broadcast(state.getClients(), message),
23959
24487
  log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
23960
24488
  });
24489
+ const autoHealer = createAutoHealer({
24490
+ projectRoot: () => state.getProjectRoot(),
24491
+ indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
24492
+ trustBoundary: deps2.trustBoundary,
24493
+ logger: deps2.logger,
24494
+ onStatus: (event) => broadcast(state.getClients(), {
24495
+ type: "connections.auto_heal_status",
24496
+ payload: event
24497
+ })
24498
+ });
24499
+ autoHealer.start();
23961
24500
  if (opts.onDispose) {
23962
- const dispose = () => kanbanSupervisor.dispose();
24501
+ const dispose = async () => {
24502
+ kanbanSupervisor.dispose();
24503
+ await autoHealer.dispose();
24504
+ };
23963
24505
  opts.onDispose(dispose);
23964
24506
  }
23965
24507
  const kanbanContext = () => ({
@@ -24823,7 +25365,11 @@ async function createPreContextServices(input) {
24823
25365
  model: config.model
24824
25366
  });
24825
25367
  context.meta["promptOnlineAgents"] = onlineAgents;
24826
- const initialContextPolicy = resolveContextWindowPolicy3(config.context);
25368
+ const initialContextPolicy = resolveContextWindowPolicy3(
25369
+ config.context,
25370
+ void 0,
25371
+ provider.capabilities?.maxContext
25372
+ );
24827
25373
  context.meta["contextWindowMode"] = initialContextPolicy.id;
24828
25374
  context.meta["contextWindowPolicy"] = initialContextPolicy;
24829
25375
  context.state.setMeta(
@@ -25262,7 +25808,7 @@ async function resolvePorts(opts) {
25262
25808
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
25263
25809
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
25264
25810
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
25265
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
25811
+ const strictPort = isStrictPort();
25266
25812
  let httpPort = requestedHttpPort;
25267
25813
  if (!strictPort) {
25268
25814
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -25481,7 +26027,7 @@ function registerShutdown(deps2) {
25481
26027
 
25482
26028
  // src/server/start-webui-companion.ts
25483
26029
  import * as http2 from "node:http";
25484
- function setupCompanionServer(httpServer, wsHost, httpPort) {
26030
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
25485
26031
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
25486
26032
  if (!companion) return null;
25487
26033
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -25492,16 +26038,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
25492
26038
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
25493
26039
  );
25494
26040
  companionServer.on("error", (err) => {
25495
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
26041
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
25496
26042
  if (!expected) {
25497
26043
  console.warn(
25498
26044
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
25499
26045
  );
25500
26046
  }
25501
26047
  });
25502
- companionServer.listen(httpPort, companion, () => {
25503
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
25504
- });
26048
+ try {
26049
+ await listenWithRetry(companionServer, companion, httpPort, { maxTries: 1 });
26050
+ } catch (err) {
26051
+ const code = err?.code ?? "unknown";
26052
+ console.warn(
26053
+ `[WebUI] companion listener on ${companionLabel} not started (${code}): ${err?.message ?? err}. The primary address is unaffected.`
26054
+ );
26055
+ return null;
26056
+ }
26057
+ console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
25505
26058
  return companionServer;
25506
26059
  }
25507
26060
 
@@ -25661,7 +26214,7 @@ function setupWebuiShutdown(options) {
25661
26214
  onPreShutdown: async () => {
25662
26215
  await options.stopEmptySessionCleanup.dispose();
25663
26216
  const disposeKanban = options.getKanbanSupervisorDispose();
25664
- disposeKanban?.();
26217
+ await disposeKanban?.();
25665
26218
  },
25666
26219
  onShutdown: async () => {
25667
26220
  unregister();
@@ -25696,6 +26249,7 @@ function setupWebuiShutdown(options) {
25696
26249
  await options.memoryStore.dispose().catch(
25697
26250
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage15(err)}`)
25698
26251
  );
26252
+ options.vectorMemoryStore?.close();
25699
26253
  await unregisterInstance(process.pid, path32.dirname(options.globalConfigPath));
25700
26254
  }
25701
26255
  });
@@ -25760,7 +26314,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
25760
26314
  async function startWebUI(opts = {}) {
25761
26315
  ensureSessionShell();
25762
26316
  const ports = await resolvePorts(opts);
25763
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
26317
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
26318
+ let httpPort = ports.httpPort;
25764
26319
  console.log("[WebUI] Starting backend services...");
25765
26320
  const boot = await bootConfig();
25766
26321
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -25788,6 +26343,27 @@ async function startWebUI(opts = {}) {
25788
26343
  );
25789
26344
  }
25790
26345
  const needsProvider = !config.provider || !config.model;
26346
+ let vectorMemoryStore;
26347
+ const vectorMemoryModelCacheDir = path33.join(
26348
+ projectRoot,
26349
+ ".wrongstack",
26350
+ "cache",
26351
+ "transformers-models"
26352
+ );
26353
+ try {
26354
+ vectorMemoryStore = new VectorMemoryStore({
26355
+ provider: new TransformersEmbeddingProvider({
26356
+ cacheDir: vectorMemoryModelCacheDir
26357
+ }),
26358
+ projectRoot
26359
+ });
26360
+ } catch (error2) {
26361
+ const message = error2 instanceof Error ? error2.message : String(error2);
26362
+ logger.warn(
26363
+ `vector memory store disabled: ${message} \u2014 standalone WebUI will run on the SAGE-only surface.`
26364
+ );
26365
+ vectorMemoryStore = void 0;
26366
+ }
25791
26367
  const preContext = await createPreContextServices({
25792
26368
  config,
25793
26369
  wpaths,
@@ -25835,6 +26411,13 @@ async function startWebUI(opts = {}) {
25835
26411
  let sessionStartedAt = preContext.sessionStartedAt;
25836
26412
  let modeId = preContext.modeId;
25837
26413
  const needsSetup = preContext.needsSetup;
26414
+ if (vectorMemoryStore) {
26415
+ void startFirstBootSageSync({
26416
+ store: vectorMemoryStore,
26417
+ memoryStore,
26418
+ logger
26419
+ });
26420
+ }
25838
26421
  const prefSnapshot2 = () => prefSnapshot(context.meta);
25839
26422
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
25840
26423
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -25955,7 +26538,12 @@ async function startWebUI(opts = {}) {
25955
26538
  events,
25956
26539
  permissionPolicy
25957
26540
  }),
25958
- distDir: opts.distDir
26541
+ distDir: opts.distDir,
26542
+ // Vector memory store — mirrors the CLI host. When `vectorMemoryStore`
26543
+ // construction failed (read-only FS, etc.) we still pass the getter;
26544
+ // it just resolves to `undefined` and the API router answers 503.
26545
+ getVectorMemoryStore: () => vectorMemoryStore,
26546
+ vectorMemoryModelCacheDir
25959
26547
  });
25960
26548
  const wsResult = createWsServers(httpServer, ports, accessToken);
25961
26549
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -26010,7 +26598,25 @@ async function startWebUI(opts = {}) {
26010
26598
  },
26011
26599
  watcherMetricsRef
26012
26600
  );
26013
- httpServer.listen(httpPort, wsHost, () => {
26601
+ const strictPort = isStrictPort();
26602
+ const boundPort = await listenWithRetry(httpServer, wsHost, httpPort, {
26603
+ maxTries: strictPort ? 1 : 10
26604
+ });
26605
+ if (boundPort !== httpPort) {
26606
+ console.warn(
26607
+ JSON.stringify({
26608
+ level: "warn",
26609
+ event: "webui.port_reassigned",
26610
+ protocol: "HTTP",
26611
+ requested: httpPort,
26612
+ assigned: boundPort,
26613
+ reason: "bind-time EADDRINUSE retry",
26614
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26615
+ })
26616
+ );
26617
+ httpPort = boundPort;
26618
+ }
26619
+ {
26014
26620
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
26015
26621
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
26016
26622
  const extraUrls = formatExternalAccessUrls({
@@ -26021,8 +26627,8 @@ async function startWebUI(opts = {}) {
26021
26627
  if (extraUrls.length > 0) {
26022
26628
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
26023
26629
  }
26024
- });
26025
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
26630
+ }
26631
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
26026
26632
  async function touchProjectEntry(root, workDir) {
26027
26633
  const resolved = path33.resolve(root);
26028
26634
  const manifest = await loadManifest(globalConfigPath);
@@ -26281,6 +26887,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26281
26887
  },
26282
26888
  codebaseIndexing,
26283
26889
  memoryStore,
26890
+ vectorMemoryStore,
26284
26891
  globalConfigPath
26285
26892
  });
26286
26893
  }