@wrongstack/webui-server 0.308.0 → 0.308.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -17177,6 +17400,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
17177
17400
  "memory.sage.listPage",
17178
17401
  "memory.sage.recover",
17179
17402
  "memory.sage.remember",
17403
+ "memory.sage.searchBreakdown",
17180
17404
  "memory.sage.update"
17181
17405
  ];
17182
17406
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -17464,6 +17688,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
17464
17688
  "memory.sage.listPage",
17465
17689
  "memory.sage.recover",
17466
17690
  "memory.sage.remember",
17691
+ "memory.sage.searchBreakdown",
17467
17692
  "memory.sage.update"
17468
17693
  ];
17469
17694
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -18150,7 +18375,12 @@ function createSessionHandlers(ctx) {
18150
18375
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
18151
18376
  );
18152
18377
  ctx.tokenCounter.reset?.();
18153
- if (usage) ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
18378
+ if (usage) {
18379
+ ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
18380
+ if (typeof usage.input === "number" && usage.input > 0) {
18381
+ ctx.context.lastRequestTokens = usage.input;
18382
+ }
18383
+ }
18154
18384
  ctx.setSessionStartedAt?.(Date.now());
18155
18385
  await ctx.onSessionSwapped?.(next.id);
18156
18386
  };
@@ -22310,6 +22540,11 @@ import {
22310
22540
  wstackGlobalRoot as wstackGlobalRoot2
22311
22541
  } from "@wrongstack/core/utils";
22312
22542
  import { ensureSessionShell } from "@wrongstack/tools";
22543
+ import {
22544
+ TransformersEmbeddingProvider,
22545
+ VectorMemoryStore,
22546
+ startFirstBootSageSync
22547
+ } from "@wrongstack/vector-memory";
22313
22548
 
22314
22549
  // src/server/backend-services.ts
22315
22550
  import { join as join12 } from "node:path";
@@ -25262,7 +25497,7 @@ async function resolvePorts(opts) {
25262
25497
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
25263
25498
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
25264
25499
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
25265
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
25500
+ const strictPort = isStrictPort();
25266
25501
  let httpPort = requestedHttpPort;
25267
25502
  if (!strictPort) {
25268
25503
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -25481,7 +25716,7 @@ function registerShutdown(deps2) {
25481
25716
 
25482
25717
  // src/server/start-webui-companion.ts
25483
25718
  import * as http2 from "node:http";
25484
- function setupCompanionServer(httpServer, wsHost, httpPort) {
25719
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
25485
25720
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
25486
25721
  if (!companion) return null;
25487
25722
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -25492,16 +25727,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
25492
25727
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
25493
25728
  );
25494
25729
  companionServer.on("error", (err) => {
25495
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
25730
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
25496
25731
  if (!expected) {
25497
25732
  console.warn(
25498
25733
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
25499
25734
  );
25500
25735
  }
25501
25736
  });
25502
- companionServer.listen(httpPort, companion, () => {
25503
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
25504
- });
25737
+ try {
25738
+ await listenWithRetry(companionServer, companion, httpPort, { maxTries: 1 });
25739
+ } catch (err) {
25740
+ const code = err?.code ?? "unknown";
25741
+ console.warn(
25742
+ `[WebUI] companion listener on ${companionLabel} not started (${code}): ${err?.message ?? err}. The primary address is unaffected.`
25743
+ );
25744
+ return null;
25745
+ }
25746
+ console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
25505
25747
  return companionServer;
25506
25748
  }
25507
25749
 
@@ -25696,6 +25938,7 @@ function setupWebuiShutdown(options) {
25696
25938
  await options.memoryStore.dispose().catch(
25697
25939
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage15(err)}`)
25698
25940
  );
25941
+ options.vectorMemoryStore?.close();
25699
25942
  await unregisterInstance(process.pid, path32.dirname(options.globalConfigPath));
25700
25943
  }
25701
25944
  });
@@ -25760,7 +26003,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
25760
26003
  async function startWebUI(opts = {}) {
25761
26004
  ensureSessionShell();
25762
26005
  const ports = await resolvePorts(opts);
25763
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
26006
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
26007
+ let httpPort = ports.httpPort;
25764
26008
  console.log("[WebUI] Starting backend services...");
25765
26009
  const boot = await bootConfig();
25766
26010
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -25788,6 +26032,27 @@ async function startWebUI(opts = {}) {
25788
26032
  );
25789
26033
  }
25790
26034
  const needsProvider = !config.provider || !config.model;
26035
+ let vectorMemoryStore;
26036
+ const vectorMemoryModelCacheDir = path33.join(
26037
+ projectRoot,
26038
+ ".wrongstack",
26039
+ "cache",
26040
+ "transformers-models"
26041
+ );
26042
+ try {
26043
+ vectorMemoryStore = new VectorMemoryStore({
26044
+ provider: new TransformersEmbeddingProvider({
26045
+ cacheDir: vectorMemoryModelCacheDir
26046
+ }),
26047
+ projectRoot
26048
+ });
26049
+ } catch (error2) {
26050
+ const message = error2 instanceof Error ? error2.message : String(error2);
26051
+ logger.warn(
26052
+ `vector memory store disabled: ${message} \u2014 standalone WebUI will run on the SAGE-only surface.`
26053
+ );
26054
+ vectorMemoryStore = void 0;
26055
+ }
25791
26056
  const preContext = await createPreContextServices({
25792
26057
  config,
25793
26058
  wpaths,
@@ -25835,6 +26100,13 @@ async function startWebUI(opts = {}) {
25835
26100
  let sessionStartedAt = preContext.sessionStartedAt;
25836
26101
  let modeId = preContext.modeId;
25837
26102
  const needsSetup = preContext.needsSetup;
26103
+ if (vectorMemoryStore) {
26104
+ void startFirstBootSageSync({
26105
+ store: vectorMemoryStore,
26106
+ memoryStore,
26107
+ logger
26108
+ });
26109
+ }
25838
26110
  const prefSnapshot2 = () => prefSnapshot(context.meta);
25839
26111
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
25840
26112
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -25955,7 +26227,12 @@ async function startWebUI(opts = {}) {
25955
26227
  events,
25956
26228
  permissionPolicy
25957
26229
  }),
25958
- distDir: opts.distDir
26230
+ distDir: opts.distDir,
26231
+ // Vector memory store — mirrors the CLI host. When `vectorMemoryStore`
26232
+ // construction failed (read-only FS, etc.) we still pass the getter;
26233
+ // it just resolves to `undefined` and the API router answers 503.
26234
+ getVectorMemoryStore: () => vectorMemoryStore,
26235
+ vectorMemoryModelCacheDir
25959
26236
  });
25960
26237
  const wsResult = createWsServers(httpServer, ports, accessToken);
25961
26238
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -26010,7 +26287,25 @@ async function startWebUI(opts = {}) {
26010
26287
  },
26011
26288
  watcherMetricsRef
26012
26289
  );
26013
- httpServer.listen(httpPort, wsHost, () => {
26290
+ const strictPort = isStrictPort();
26291
+ const boundPort = await listenWithRetry(httpServer, wsHost, httpPort, {
26292
+ maxTries: strictPort ? 1 : 10
26293
+ });
26294
+ if (boundPort !== httpPort) {
26295
+ console.warn(
26296
+ JSON.stringify({
26297
+ level: "warn",
26298
+ event: "webui.port_reassigned",
26299
+ protocol: "HTTP",
26300
+ requested: httpPort,
26301
+ assigned: boundPort,
26302
+ reason: "bind-time EADDRINUSE retry",
26303
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26304
+ })
26305
+ );
26306
+ httpPort = boundPort;
26307
+ }
26308
+ {
26014
26309
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
26015
26310
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
26016
26311
  const extraUrls = formatExternalAccessUrls({
@@ -26021,8 +26316,8 @@ async function startWebUI(opts = {}) {
26021
26316
  if (extraUrls.length > 0) {
26022
26317
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
26023
26318
  }
26024
- });
26025
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
26319
+ }
26320
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
26026
26321
  async function touchProjectEntry(root, workDir) {
26027
26322
  const resolved = path33.resolve(root);
26028
26323
  const manifest = await loadManifest(globalConfigPath);
@@ -26281,6 +26576,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26281
26576
  },
26282
26577
  codebaseIndexing,
26283
26578
  memoryStore,
26579
+ vectorMemoryStore,
26284
26580
  globalConfigPath
26285
26581
  });
26286
26582
  }