@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.
package/dist/index.js CHANGED
@@ -7376,11 +7376,11 @@ async function handleGitDiff(ws, projectRoot, path39) {
7376
7376
  try {
7377
7377
  const git = makeGit(cwd);
7378
7378
  const { readFile: readFile13 } = await import("node:fs/promises");
7379
- const { join: join18 } = await import("node:path");
7379
+ const { join: join19 } = await import("node:path");
7380
7380
  const oldText = await git(["show", `HEAD:${path39}`]);
7381
7381
  let newText = "";
7382
7382
  try {
7383
- const abs = cwd ? join18(cwd, path39) : path39;
7383
+ const abs = cwd ? join19(cwd, path39) : path39;
7384
7384
  const buf = await readFile13(abs);
7385
7385
  if (buf.includes(0)) {
7386
7386
  reply2({ oldText: "", newText: "", binary: true });
@@ -10411,6 +10411,7 @@ function strictDecodeParam(segment, res) {
10411
10411
  }
10412
10412
 
10413
10413
  // src/server/http-server/vector-memory-handlers.ts
10414
+ import { getSageSurface } from "@wrongstack/sage";
10414
10415
  import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
10415
10416
  function snapshotVectorMemory(store, opts = {}) {
10416
10417
  const stats = store.stats();
@@ -10420,6 +10421,9 @@ function snapshotVectorMemory(store, opts = {}) {
10420
10421
  stats
10421
10422
  };
10422
10423
  }
10424
+ function snapshotVectorMemoryCache(store) {
10425
+ return store.cacheStats();
10426
+ }
10423
10427
  async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10424
10428
  const store = getStore();
10425
10429
  if (!store) {
@@ -10439,7 +10443,8 @@ async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10439
10443
  dimensions: snap.stats.dimensions,
10440
10444
  entries: snap.stats.entries,
10441
10445
  vectors: snap.stats.vectors,
10442
- providers: snap.stats.providers
10446
+ providers: snap.stats.providers,
10447
+ cache: snapshotVectorMemoryCache(store)
10443
10448
  };
10444
10449
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10445
10450
  res.end(JSON.stringify(body));
@@ -10459,7 +10464,29 @@ function parseSearchParams(url) {
10459
10464
  const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10));
10460
10465
  const rawThreshold = url.searchParams.get("threshold");
10461
10466
  const threshold = rawThreshold !== null && rawThreshold !== "" ? Math.max(0, Math.min(1, Number.parseFloat(rawThreshold))) : void 0;
10462
- return { query, limit, threshold: Number.isFinite(threshold) ? threshold : void 0 };
10467
+ const similarity = url.searchParams.get("similarity") === "1";
10468
+ return { query, limit, threshold: Number.isFinite(threshold) ? threshold : void 0, similarity };
10469
+ }
10470
+ function cosineMatrix(vectors) {
10471
+ const n = vectors.length;
10472
+ const out = new Array(n);
10473
+ for (let i = 0; i < n; i++) {
10474
+ out[i] = new Array(n).fill(0);
10475
+ }
10476
+ for (let i = 0; i < n; i++) {
10477
+ out[i][i] = 1;
10478
+ for (let j = i + 1; j < n; j++) {
10479
+ const a = vectors[i];
10480
+ const b = vectors[j];
10481
+ let dot = 0;
10482
+ const len = Math.min(a.length, b.length);
10483
+ for (let k = 0; k < len; k++) dot += (a[k] ?? 0) * (b[k] ?? 0);
10484
+ const score = Math.max(0, Math.min(1, dot));
10485
+ out[i][j] = score;
10486
+ out[j][i] = score;
10487
+ }
10488
+ }
10489
+ return out;
10463
10490
  }
10464
10491
  async function handleVectorMemorySearch(res, url, getStore) {
10465
10492
  const store = getStore();
@@ -10468,7 +10495,7 @@ async function handleVectorMemorySearch(res, url, getStore) {
10468
10495
  res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10469
10496
  return;
10470
10497
  }
10471
- const { query, limit, threshold } = parseSearchParams(url);
10498
+ const { query, limit, threshold, similarity } = parseSearchParams(url);
10472
10499
  if (query.trim().length === 0) {
10473
10500
  res.writeHead(400, { "Content-Type": "application/json" });
10474
10501
  res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
@@ -10477,7 +10504,8 @@ async function handleVectorMemorySearch(res, url, getStore) {
10477
10504
  try {
10478
10505
  const hits = await store.search(query, {
10479
10506
  limit,
10480
- ...threshold !== void 0 ? { threshold } : {}
10507
+ ...threshold !== void 0 ? { threshold } : {},
10508
+ includeVectors: similarity
10481
10509
  });
10482
10510
  const body = {
10483
10511
  hits: hits.map((h) => ({
@@ -10489,6 +10517,12 @@ async function handleVectorMemorySearch(res, url, getStore) {
10489
10517
  })),
10490
10518
  count: hits.length
10491
10519
  };
10520
+ if (similarity && hits.length > 1) {
10521
+ const vecs = hits.map((h) => h.vector).filter((v) => v !== void 0);
10522
+ if (vecs.length === hits.length) {
10523
+ body.similarity = cosineMatrix(vecs);
10524
+ }
10525
+ }
10492
10526
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10493
10527
  res.end(JSON.stringify(body));
10494
10528
  } catch (error2) {
@@ -10573,7 +10607,7 @@ async function handleVectorMemoryForget(res, url, getStore) {
10573
10607
  const id = match ? strictDecodeParam(decodeSessionId(match[1]), res) : null;
10574
10608
  if (id === null) return;
10575
10609
  try {
10576
- const removed = store.forget(id);
10610
+ const removed = await store.forget(id);
10577
10611
  res.writeHead(200, { "Content-Type": "application/json" });
10578
10612
  res.end(JSON.stringify({ removed }));
10579
10613
  } catch (error2) {
@@ -10586,6 +10620,86 @@ async function handleVectorMemoryForget(res, url, getStore) {
10586
10620
  );
10587
10621
  }
10588
10622
  }
10623
+ function parseMemorySearchParams(url) {
10624
+ const query = url.searchParams.get("q") ?? "";
10625
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10);
10626
+ const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 20));
10627
+ const explain = url.searchParams.get("explain") === "1";
10628
+ return { query, limit, explain };
10629
+ }
10630
+ async function handleMemorySearch(res, url, getStore) {
10631
+ const store = getStore();
10632
+ if (!store) {
10633
+ res.writeHead(503, { "Content-Type": "application/json" });
10634
+ res.end(JSON.stringify({ error: "Memory store not enabled in this host" }));
10635
+ return;
10636
+ }
10637
+ const { query, limit, explain } = parseMemorySearchParams(url);
10638
+ if (query.trim().length === 0) {
10639
+ res.writeHead(400, { "Content-Type": "application/json" });
10640
+ res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
10641
+ return;
10642
+ }
10643
+ const Sage = getSageSurface(store);
10644
+ if (!Sage) {
10645
+ res.writeHead(503, { "Content-Type": "application/json" });
10646
+ res.end(
10647
+ JSON.stringify({
10648
+ error: "Memory search requires the SAGE surface (this host does not expose it)."
10649
+ })
10650
+ );
10651
+ return;
10652
+ }
10653
+ try {
10654
+ let payload;
10655
+ if (explain && typeof Sage.searchSageWithBreakdown === "function") {
10656
+ const hits = await Sage.searchSageWithBreakdown(query, { limit });
10657
+ payload = {
10658
+ count: hits.length,
10659
+ channel: "breakdown",
10660
+ hits: hits.map((h) => ({
10661
+ id: h.memory.id,
10662
+ text: h.memory.text,
10663
+ kind: h.memory.kind,
10664
+ status: h.memory.status,
10665
+ tags: h.memory.tags ?? [],
10666
+ lexicalScore: h.lexicalScore,
10667
+ vectorScore: h.vectorScore,
10668
+ finalScore: h.finalScore,
10669
+ source: h.source
10670
+ }))
10671
+ };
10672
+ } else {
10673
+ const rows = await Sage.searchSage(query, { limit });
10674
+ const total = rows.length;
10675
+ payload = {
10676
+ count: total,
10677
+ channel: "lexical",
10678
+ hits: rows.map((memory, index) => ({
10679
+ id: memory.id,
10680
+ text: memory.text,
10681
+ kind: memory.kind,
10682
+ status: memory.status,
10683
+ tags: memory.tags ?? [],
10684
+ lexicalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
10685
+ vectorScore: null,
10686
+ finalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
10687
+ source: "lexical"
10688
+ }))
10689
+ };
10690
+ }
10691
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10692
+ res.end(JSON.stringify(payload));
10693
+ } catch (error2) {
10694
+ res.writeHead(500, { "Content-Type": "application/json" });
10695
+ res.end(
10696
+ JSON.stringify({
10697
+ error: "Memory search failed",
10698
+ detail: sanitizeApiError2(error2)
10699
+ })
10700
+ );
10701
+ }
10702
+ }
10589
10703
 
10590
10704
  // src/server/http-server/api-router.ts
10591
10705
  async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessTokenOk, getTechStackRuntime) {
@@ -11036,6 +11150,15 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
11036
11150
  await handleVectorMemoryStore(res, req, () => deps2.getVectorMemoryStore?.());
11037
11151
  return true;
11038
11152
  }
11153
+ if (url.pathname === "/api/memory/search" && req.method === "GET") {
11154
+ if (requireAccessToken && !accessTokenOk) {
11155
+ res.writeHead(401, { "Content-Type": "application/json" });
11156
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11157
+ return true;
11158
+ }
11159
+ await handleMemorySearch(res, url, () => deps2.getMemoryStore?.());
11160
+ return true;
11161
+ }
11039
11162
  if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
11040
11163
  await handleDeadCodeActionPlan(
11041
11164
  res,
@@ -11200,12 +11323,15 @@ function createHttpServer(opts) {
11200
11323
  distDir,
11201
11324
  url,
11202
11325
  opts,
11203
- port,
11326
+ // Live port from the socket: the bind may have advanced past an
11327
+ // EADDRINUSE (listenWithRetry) after this server was constructed,
11328
+ // and the CSP must advertise the port actually serving this request.
11329
+ res.socket?.localPort ?? port,
11204
11330
  shouldSetAuthCookie
11205
11331
  );
11206
11332
  } catch (err) {
11207
11333
  if (err.code === "ENOENT") {
11208
- await handleSpaFallback(res, distDir, opts, port);
11334
+ await handleSpaFallback(res, distDir, opts, res.socket?.localPort ?? port);
11209
11335
  } else {
11210
11336
  console.error({ url: req.url, err });
11211
11337
  res.writeHead(500);
@@ -14136,13 +14262,13 @@ async function handleMcpRoute(ws, msg, handlers) {
14136
14262
  }
14137
14263
 
14138
14264
  // src/server/memory-handlers.ts
14139
- import { getSageSurface } from "@wrongstack/sage";
14265
+ import { getSageSurface as getSageSurface2 } from "@wrongstack/sage";
14140
14266
  function requiresSage(command) {
14141
14267
  return `\`${command}\` requires the SAGE backend (Sage.enabled).`;
14142
14268
  }
14143
14269
  async function handleMemoryList(ws, memoryStore) {
14144
14270
  try {
14145
- const Sage = getSageSurface(memoryStore);
14271
+ const Sage = getSageSurface2(memoryStore);
14146
14272
  if (Sage) {
14147
14273
  const [stats, memories] = await Promise.all([Sage.stats(), Sage.listSage()]);
14148
14274
  const text3 = memories.length === 0 ? "\u{1F9E0} SAGE is empty." : formatSageText(stats, memories);
@@ -14177,7 +14303,7 @@ function formatSageText(stats, memories) {
14177
14303
  return lines.join("\n");
14178
14304
  }
14179
14305
  async function handleSageList(ws, memoryStore) {
14180
- const Sage = getSageSurface(memoryStore);
14306
+ const Sage = getSageSurface2(memoryStore);
14181
14307
  if (!Sage) {
14182
14308
  send(ws, {
14183
14309
  type: "memory.sage.list",
@@ -14193,7 +14319,7 @@ async function handleSageList(ws, memoryStore) {
14193
14319
  }
14194
14320
  }
14195
14321
  async function handleSageListPage(ws, msg, memoryStore) {
14196
- const Sage = getSageSurface(memoryStore);
14322
+ const Sage = getSageSurface2(memoryStore);
14197
14323
  if (!Sage) {
14198
14324
  send(ws, {
14199
14325
  type: "memory.sage.listPage",
@@ -14243,8 +14369,50 @@ async function handleSageListPage(ws, msg, memoryStore) {
14243
14369
  send(ws, { type: "memory.sage.listPage", payload: { error: errMessage(err) } });
14244
14370
  }
14245
14371
  }
14372
+ async function handleSageSearchBreakdown(ws, msg, memoryStore) {
14373
+ const Sage = getSageSurface2(memoryStore);
14374
+ if (!Sage) {
14375
+ send(ws, {
14376
+ type: "memory.sage.searchBreakdown",
14377
+ payload: { error: requiresSage("memory.sage.searchBreakdown") }
14378
+ });
14379
+ return;
14380
+ }
14381
+ try {
14382
+ const payload = msg.payload ?? {};
14383
+ const query = typeof payload["query"] === "string" ? payload["query"] : "";
14384
+ if (query.trim().length === 0) {
14385
+ send(ws, {
14386
+ type: "memory.sage.searchBreakdown",
14387
+ payload: { error: "Missing required field `query`" }
14388
+ });
14389
+ return;
14390
+ }
14391
+ const limit = typeof payload["limit"] === "number" ? payload["limit"] : 20;
14392
+ const includeStale = payload["includeStale"] === true;
14393
+ const includeStatuses = includeStale ? ["active", "stale"] : ["active"];
14394
+ const options = { limit, includeStatuses };
14395
+ if (typeof Sage.searchSageWithBreakdown === "function") {
14396
+ const hits2 = await Sage.searchSageWithBreakdown(query, options);
14397
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { hits: hits2, source: "breakdown" } });
14398
+ return;
14399
+ }
14400
+ const rows = await Sage.searchSage(query, options);
14401
+ const total = rows.length;
14402
+ const hits = rows.map((memory, index) => ({
14403
+ memory,
14404
+ vectorScore: null,
14405
+ lexicalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
14406
+ finalScore: total <= 1 ? 1 : 1 - index / Math.max(1, total - 1),
14407
+ source: "lexical"
14408
+ }));
14409
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { hits, source: "lexical" } });
14410
+ } catch (err) {
14411
+ send(ws, { type: "memory.sage.searchBreakdown", payload: { error: errMessage(err) } });
14412
+ }
14413
+ }
14246
14414
  async function handleSageGet(ws, msg, memoryStore) {
14247
- const Sage = getSageSurface(memoryStore);
14415
+ const Sage = getSageSurface2(memoryStore);
14248
14416
  if (!Sage) {
14249
14417
  send(ws, {
14250
14418
  type: "memory.sage.get",
@@ -14269,7 +14437,7 @@ async function handleSageGet(ws, msg, memoryStore) {
14269
14437
  }
14270
14438
  }
14271
14439
  async function handleSageGraph(ws, msg, memoryStore) {
14272
- const Sage = getSageSurface(memoryStore);
14440
+ const Sage = getSageSurface2(memoryStore);
14273
14441
  if (!Sage?.graphFor) {
14274
14442
  send(ws, {
14275
14443
  type: "memory.sage.graph",
@@ -14303,7 +14471,7 @@ async function handleSageGraph(ws, msg, memoryStore) {
14303
14471
  }
14304
14472
  }
14305
14473
  async function handleSageUpdate(ws, msg, memoryStore) {
14306
- const Sage = getSageSurface(memoryStore);
14474
+ const Sage = getSageSurface2(memoryStore);
14307
14475
  if (!Sage) {
14308
14476
  send(ws, {
14309
14477
  type: "memory.sage.update",
@@ -14331,7 +14499,7 @@ async function handleSageUpdate(ws, msg, memoryStore) {
14331
14499
  }
14332
14500
  }
14333
14501
  async function handleSageRemember(ws, msg, memoryStore) {
14334
- const Sage = getSageSurface(memoryStore);
14502
+ const Sage = getSageSurface2(memoryStore);
14335
14503
  if (!Sage) {
14336
14504
  send(ws, {
14337
14505
  type: "memory.sage.remember",
@@ -14365,7 +14533,7 @@ async function handleSageRemember(ws, msg, memoryStore) {
14365
14533
  }
14366
14534
  }
14367
14535
  async function handleSageDelete(ws, msg, memoryStore) {
14368
- const Sage = getSageSurface(memoryStore);
14536
+ const Sage = getSageSurface2(memoryStore);
14369
14537
  if (!Sage) {
14370
14538
  send(ws, {
14371
14539
  type: "memory.sage.delete",
@@ -14398,7 +14566,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
14398
14566
  }
14399
14567
  }
14400
14568
  async function handleSageRecover(ws, msg, memoryStore) {
14401
- const Sage = getSageSurface(memoryStore);
14569
+ const Sage = getSageSurface2(memoryStore);
14402
14570
  if (!Sage?.recoverSage) {
14403
14571
  send(ws, {
14404
14572
  type: "memory.sage.recover",
@@ -14442,7 +14610,7 @@ async function handleSageRecover(ws, msg, memoryStore) {
14442
14610
  }
14443
14611
  }
14444
14612
  async function handleSageListCandidates(ws, msg, memoryStore) {
14445
- const Sage = getSageSurface(memoryStore);
14613
+ const Sage = getSageSurface2(memoryStore);
14446
14614
  if (!Sage) {
14447
14615
  send(ws, {
14448
14616
  type: "memory.sage.listCandidates",
@@ -14470,7 +14638,7 @@ async function handleSageListCandidates(ws, msg, memoryStore) {
14470
14638
  }
14471
14639
  }
14472
14640
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
14473
- const Sage = getSageSurface(memoryStore);
14641
+ const Sage = getSageSurface2(memoryStore);
14474
14642
  if (!Sage) {
14475
14643
  send(ws, {
14476
14644
  type: "memory.sage.candidateResolve",
@@ -14527,7 +14695,7 @@ async function handleSageCandidateResolve(ws, msg, memoryStore) {
14527
14695
  }
14528
14696
  }
14529
14697
  async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14530
- const Sage = getSageSurface(memoryStore);
14698
+ const Sage = getSageSurface2(memoryStore);
14531
14699
  if (!Sage?.backfillRecoverable) {
14532
14700
  send(ws, {
14533
14701
  type: "memory.sage.backfillRecoverable",
@@ -14567,7 +14735,7 @@ async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14567
14735
  }
14568
14736
  }
14569
14737
  async function handleSageForFile(ws, msg, memoryStore) {
14570
- const Sage = getSageSurface(memoryStore);
14738
+ const Sage = getSageSurface2(memoryStore);
14571
14739
  if (!Sage?.findMemoriesForFile) {
14572
14740
  send(ws, {
14573
14741
  type: "memory.sage.forFile",
@@ -14659,6 +14827,9 @@ async function handleMemoryRoute(ctx, ws, message) {
14659
14827
  case "memory.sage.forFile":
14660
14828
  await handleSageForFile(ws, message, store);
14661
14829
  return true;
14830
+ case "memory.sage.searchBreakdown":
14831
+ await handleSageSearchBreakdown(ws, message, store);
14832
+ return true;
14662
14833
  default:
14663
14834
  return false;
14664
14835
  }
@@ -15049,23 +15220,31 @@ var SURFACE_DEFAULT_PORTS = {
15049
15220
  webui: { http: 3456 },
15050
15221
  simpleui: { http: 3466 }
15051
15222
  };
15223
+ var MAX_TCP_PORT = 65535;
15052
15224
  function surfaceLabel(surface) {
15053
15225
  return surface === "webui" ? "WebUI" : "SimpleUI";
15054
15226
  }
15227
+ function isStrictPort() {
15228
+ const value = process.env["WEBUI_STRICT_PORT"];
15229
+ return value === "1" || value === "true";
15230
+ }
15055
15231
  function getSurfaceDefaultPorts(surface) {
15056
15232
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
15057
15233
  }
15058
15234
  function isPortFree(host, port) {
15235
+ return probePort(host, port).then((err) => err === null);
15236
+ }
15237
+ function probePort(host, port) {
15059
15238
  return new Promise((resolve20) => {
15060
15239
  const srv = net2.createServer();
15061
- srv.once("error", () => resolve20(false));
15240
+ srv.once("error", (err) => resolve20(err));
15062
15241
  srv.once("listening", () => {
15063
- srv.close(() => resolve20(true));
15242
+ srv.close(() => resolve20(null));
15064
15243
  });
15065
15244
  try {
15066
15245
  srv.listen(port, host);
15067
- } catch {
15068
- resolve20(false);
15246
+ } catch (err) {
15247
+ resolve20(err);
15069
15248
  }
15070
15249
  });
15071
15250
  }
@@ -15074,7 +15253,7 @@ async function findFreePort(host, startPort, opts = {}) {
15074
15253
  const maxTries = opts.maxTries ?? 200;
15075
15254
  let port = startPort;
15076
15255
  for (let i = 0; i < maxTries; i++) {
15077
- if (port > 65535) port = 1024 + port % 5e4;
15256
+ if (port > MAX_TCP_PORT) port = 1024 + port % 5e4;
15078
15257
  if (!exclude.has(port) && await isPortFree(host, port)) {
15079
15258
  return port;
15080
15259
  }
@@ -15085,6 +15264,50 @@ async function findFreePort(host, startPort, opts = {}) {
15085
15264
  field: "port"
15086
15265
  });
15087
15266
  }
15267
+ function listenWithRetry(server, host, port, opts = {}) {
15268
+ const maxTries = opts.maxTries ?? 10;
15269
+ return new Promise((resolve20, reject) => {
15270
+ const canAdvance = (candidate) => candidate < MAX_TCP_PORT;
15271
+ const probeable = (candidate) => Number.isInteger(candidate) && candidate >= 0 && candidate <= MAX_TCP_PORT;
15272
+ const attempt = (candidate, remaining) => {
15273
+ void (async () => {
15274
+ if (probeable(candidate)) {
15275
+ const probeErr = await probePort(host, candidate);
15276
+ if (probeErr !== null) {
15277
+ if (probeErr.code === "EADDRINUSE" && remaining > 1 && canAdvance(candidate)) {
15278
+ attempt(candidate + 1, remaining - 1);
15279
+ return;
15280
+ }
15281
+ reject(probeErr);
15282
+ return;
15283
+ }
15284
+ }
15285
+ const onError = (err) => {
15286
+ server.off("listening", onListening);
15287
+ server.off("error", onError);
15288
+ if (err.code === "EADDRINUSE" && remaining > 1 && canAdvance(candidate)) {
15289
+ attempt(candidate + 1, remaining - 1);
15290
+ return;
15291
+ }
15292
+ reject(err);
15293
+ };
15294
+ const onListening = () => {
15295
+ server.off("error", onError);
15296
+ const address = server.address();
15297
+ resolve20(address && typeof address === "object" ? address.port : candidate);
15298
+ };
15299
+ server.once("error", onError);
15300
+ server.once("listening", onListening);
15301
+ try {
15302
+ server.listen(candidate, host);
15303
+ } catch (err) {
15304
+ onError(err);
15305
+ }
15306
+ })();
15307
+ };
15308
+ attempt(port, maxTries);
15309
+ });
15310
+ }
15088
15311
 
15089
15312
  // src/server/frontend-static-serve.ts
15090
15313
  import { spawn as spawn2 } from "node:child_process";
@@ -15218,7 +15441,10 @@ async function startStaticServe(opts, deps2 = {}) {
15218
15441
  ...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
15219
15442
  });
15220
15443
  if (!opts.deferListen) {
15221
- server.listen(opts.httpPort, opts.host);
15444
+ const boundPort = await listenWithRetry(server, opts.host, opts.httpPort, {
15445
+ maxTries: opts.strictPort ? 1 : 10
15446
+ });
15447
+ return { server, port: boundPort };
15222
15448
  }
15223
15449
  return { server, port: opts.httpPort };
15224
15450
  }
@@ -15351,7 +15577,7 @@ function announceWebuiReady(p) {
15351
15577
  token: p.wsToken,
15352
15578
  publicUrl: p.publicUrl
15353
15579
  });
15354
- p.server.on("listening", () => {
15580
+ const announce = () => {
15355
15581
  const extraUrls = formatExternalAccessUrls({
15356
15582
  bindHost: p.host,
15357
15583
  port: p.httpPort,
@@ -15368,7 +15594,12 @@ ${extraUrls.join("\n")}
15368
15594
  ${extraBlock}`
15369
15595
  );
15370
15596
  if (p.open) launch(openUrl);
15371
- });
15597
+ };
15598
+ if (p.server.listening) {
15599
+ announce();
15600
+ return;
15601
+ }
15602
+ p.server.on("listening", announce);
15372
15603
  }
15373
15604
  var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
15374
15605
  async function runBounded(work, timeoutMs, label, debug) {
@@ -18459,6 +18690,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
18459
18690
  "memory.sage.listPage",
18460
18691
  "memory.sage.recover",
18461
18692
  "memory.sage.remember",
18693
+ "memory.sage.searchBreakdown",
18462
18694
  "memory.sage.update"
18463
18695
  ];
18464
18696
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -18746,6 +18978,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
18746
18978
  "memory.sage.listPage",
18747
18979
  "memory.sage.recover",
18748
18980
  "memory.sage.remember",
18981
+ "memory.sage.searchBreakdown",
18749
18982
  "memory.sage.update"
18750
18983
  ];
18751
18984
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -19623,7 +19856,12 @@ function createSessionHandlers(ctx) {
19623
19856
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
19624
19857
  );
19625
19858
  ctx.tokenCounter.reset?.();
19626
- if (usage) ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
19859
+ if (usage) {
19860
+ ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
19861
+ if (typeof usage.input === "number" && usage.input > 0) {
19862
+ ctx.context.lastRequestTokens = usage.input;
19863
+ }
19864
+ }
19627
19865
  ctx.setSessionStartedAt?.(Date.now());
19628
19866
  await ctx.onSessionSwapped?.(next.id);
19629
19867
  };
@@ -24434,6 +24672,11 @@ import {
24434
24672
  wstackGlobalRoot as wstackGlobalRoot4
24435
24673
  } from "@wrongstack/core/utils";
24436
24674
  import { ensureSessionShell } from "@wrongstack/tools";
24675
+ import {
24676
+ TransformersEmbeddingProvider,
24677
+ VectorMemoryStore,
24678
+ startFirstBootSageSync
24679
+ } from "@wrongstack/vector-memory";
24437
24680
 
24438
24681
  // src/server/backend-services.ts
24439
24682
  import { join as join15 } from "node:path";
@@ -27386,7 +27629,7 @@ async function resolvePorts(opts) {
27386
27629
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
27387
27630
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
27388
27631
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
27389
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
27632
+ const strictPort = isStrictPort();
27390
27633
  let httpPort = requestedHttpPort;
27391
27634
  if (!strictPort) {
27392
27635
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -27605,7 +27848,7 @@ function registerShutdown(deps2) {
27605
27848
 
27606
27849
  // src/server/start-webui-companion.ts
27607
27850
  import * as http2 from "node:http";
27608
- function setupCompanionServer(httpServer, wsHost, httpPort) {
27851
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
27609
27852
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
27610
27853
  if (!companion) return null;
27611
27854
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -27616,16 +27859,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
27616
27859
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
27617
27860
  );
27618
27861
  companionServer.on("error", (err) => {
27619
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
27862
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
27620
27863
  if (!expected) {
27621
27864
  console.warn(
27622
27865
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
27623
27866
  );
27624
27867
  }
27625
27868
  });
27626
- companionServer.listen(httpPort, companion, () => {
27627
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
27628
- });
27869
+ try {
27870
+ await listenWithRetry(companionServer, companion, httpPort, { maxTries: 1 });
27871
+ } catch (err) {
27872
+ const code = err?.code ?? "unknown";
27873
+ console.warn(
27874
+ `[WebUI] companion listener on ${companionLabel} not started (${code}): ${err?.message ?? err}. The primary address is unaffected.`
27875
+ );
27876
+ return null;
27877
+ }
27878
+ console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
27629
27879
  return companionServer;
27630
27880
  }
27631
27881
 
@@ -27820,6 +28070,7 @@ function setupWebuiShutdown(options) {
27820
28070
  await options.memoryStore.dispose().catch(
27821
28071
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage16(err)}`)
27822
28072
  );
28073
+ options.vectorMemoryStore?.close();
27823
28074
  await unregisterInstance(process.pid, path37.dirname(options.globalConfigPath));
27824
28075
  }
27825
28076
  });
@@ -27884,7 +28135,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
27884
28135
  async function startWebUI(opts = {}) {
27885
28136
  ensureSessionShell();
27886
28137
  const ports = await resolvePorts(opts);
27887
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
28138
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
28139
+ let httpPort = ports.httpPort;
27888
28140
  console.log("[WebUI] Starting backend services...");
27889
28141
  const boot = await bootConfig();
27890
28142
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -27912,6 +28164,27 @@ async function startWebUI(opts = {}) {
27912
28164
  );
27913
28165
  }
27914
28166
  const needsProvider = !config.provider || !config.model;
28167
+ let vectorMemoryStore;
28168
+ const vectorMemoryModelCacheDir = path38.join(
28169
+ projectRoot,
28170
+ ".wrongstack",
28171
+ "cache",
28172
+ "transformers-models"
28173
+ );
28174
+ try {
28175
+ vectorMemoryStore = new VectorMemoryStore({
28176
+ provider: new TransformersEmbeddingProvider({
28177
+ cacheDir: vectorMemoryModelCacheDir
28178
+ }),
28179
+ projectRoot
28180
+ });
28181
+ } catch (error2) {
28182
+ const message = error2 instanceof Error ? error2.message : String(error2);
28183
+ logger.warn(
28184
+ `vector memory store disabled: ${message} \u2014 standalone WebUI will run on the SAGE-only surface.`
28185
+ );
28186
+ vectorMemoryStore = void 0;
28187
+ }
27915
28188
  const preContext = await createPreContextServices({
27916
28189
  config,
27917
28190
  wpaths,
@@ -27959,6 +28232,13 @@ async function startWebUI(opts = {}) {
27959
28232
  let sessionStartedAt = preContext.sessionStartedAt;
27960
28233
  let modeId = preContext.modeId;
27961
28234
  const needsSetup = preContext.needsSetup;
28235
+ if (vectorMemoryStore) {
28236
+ void startFirstBootSageSync({
28237
+ store: vectorMemoryStore,
28238
+ memoryStore,
28239
+ logger
28240
+ });
28241
+ }
27962
28242
  const prefSnapshot2 = () => prefSnapshot(context.meta);
27963
28243
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
27964
28244
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -28079,7 +28359,12 @@ async function startWebUI(opts = {}) {
28079
28359
  events,
28080
28360
  permissionPolicy
28081
28361
  }),
28082
- distDir: opts.distDir
28362
+ distDir: opts.distDir,
28363
+ // Vector memory store — mirrors the CLI host. When `vectorMemoryStore`
28364
+ // construction failed (read-only FS, etc.) we still pass the getter;
28365
+ // it just resolves to `undefined` and the API router answers 503.
28366
+ getVectorMemoryStore: () => vectorMemoryStore,
28367
+ vectorMemoryModelCacheDir
28083
28368
  });
28084
28369
  const wsResult = createWsServers(httpServer, ports, accessToken);
28085
28370
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -28134,7 +28419,25 @@ async function startWebUI(opts = {}) {
28134
28419
  },
28135
28420
  watcherMetricsRef
28136
28421
  );
28137
- httpServer.listen(httpPort, wsHost, () => {
28422
+ const strictPort = isStrictPort();
28423
+ const boundPort = await listenWithRetry(httpServer, wsHost, httpPort, {
28424
+ maxTries: strictPort ? 1 : 10
28425
+ });
28426
+ if (boundPort !== httpPort) {
28427
+ console.warn(
28428
+ JSON.stringify({
28429
+ level: "warn",
28430
+ event: "webui.port_reassigned",
28431
+ protocol: "HTTP",
28432
+ requested: httpPort,
28433
+ assigned: boundPort,
28434
+ reason: "bind-time EADDRINUSE retry",
28435
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
28436
+ })
28437
+ );
28438
+ httpPort = boundPort;
28439
+ }
28440
+ {
28138
28441
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
28139
28442
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
28140
28443
  const extraUrls = formatExternalAccessUrls({
@@ -28145,8 +28448,8 @@ async function startWebUI(opts = {}) {
28145
28448
  if (extraUrls.length > 0) {
28146
28449
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
28147
28450
  }
28148
- });
28149
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
28451
+ }
28452
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
28150
28453
  async function touchProjectEntry(root, workDir) {
28151
28454
  const resolved = path38.resolve(root);
28152
28455
  const manifest = await loadManifest(globalConfigPath);
@@ -28405,6 +28708,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
28405
28708
  },
28406
28709
  codebaseIndexing,
28407
28710
  memoryStore,
28711
+ vectorMemoryStore,
28408
28712
  globalConfigPath
28409
28713
  });
28410
28714
  }
@@ -28638,9 +28942,11 @@ export {
28638
28942
  isPidAlive,
28639
28943
  isPortFree,
28640
28944
  isRegisteredMessageType,
28945
+ isStrictPort,
28641
28946
  isWildcardBind,
28642
28947
  joinSessionRegistryWithWebUIInstances,
28643
28948
  listInstances,
28949
+ listenWithRetry,
28644
28950
  loadManifest,
28645
28951
  loadSavedProviders,
28646
28952
  markConnectionActivity,