@wrongstack/webui-server 0.307.1 → 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 });
@@ -8983,7 +8983,7 @@ function verifyClient(input) {
8983
8983
 
8984
8984
  // src/server/http-server/api-router.ts
8985
8985
  import * as v8 from "node:v8";
8986
- import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
8986
+ import { sanitizeApiError as sanitizeApiError3 } from "@wrongstack/core/security";
8987
8987
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
8988
8988
 
8989
8989
  // src/server/codemap-handlers.ts
@@ -10410,6 +10410,297 @@ function strictDecodeParam(segment, res) {
10410
10410
  }
10411
10411
  }
10412
10412
 
10413
+ // src/server/http-server/vector-memory-handlers.ts
10414
+ import { getSageSurface } from "@wrongstack/sage";
10415
+ import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
10416
+ function snapshotVectorMemory(store, opts = {}) {
10417
+ const stats = store.stats();
10418
+ return {
10419
+ storePath: opts.projectRoot,
10420
+ modelCacheDir: opts.modelCacheDir,
10421
+ stats
10422
+ };
10423
+ }
10424
+ function snapshotVectorMemoryCache(store) {
10425
+ return store.cacheStats();
10426
+ }
10427
+ async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10428
+ const store = getStore();
10429
+ if (!store) {
10430
+ const body = { enabled: false };
10431
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10432
+ res.end(JSON.stringify(body));
10433
+ return;
10434
+ }
10435
+ try {
10436
+ const snap = snapshotVectorMemory(store, opts);
10437
+ const body = {
10438
+ enabled: true,
10439
+ storePath: snap.storePath,
10440
+ modelCacheDir: snap.modelCacheDir,
10441
+ providerId: snap.stats.modelId,
10442
+ modelId: snap.stats.modelId,
10443
+ dimensions: snap.stats.dimensions,
10444
+ entries: snap.stats.entries,
10445
+ vectors: snap.stats.vectors,
10446
+ providers: snap.stats.providers,
10447
+ cache: snapshotVectorMemoryCache(store)
10448
+ };
10449
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10450
+ res.end(JSON.stringify(body));
10451
+ } catch (error2) {
10452
+ res.writeHead(500, { "Content-Type": "application/json" });
10453
+ res.end(
10454
+ JSON.stringify({
10455
+ error: "Vector memory status failed",
10456
+ detail: sanitizeApiError2(error2)
10457
+ })
10458
+ );
10459
+ }
10460
+ }
10461
+ function parseSearchParams(url) {
10462
+ const query = url.searchParams.get("q") ?? "";
10463
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
10464
+ const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10));
10465
+ const rawThreshold = url.searchParams.get("threshold");
10466
+ const threshold = rawThreshold !== null && rawThreshold !== "" ? Math.max(0, Math.min(1, Number.parseFloat(rawThreshold))) : 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;
10490
+ }
10491
+ async function handleVectorMemorySearch(res, url, getStore) {
10492
+ const store = getStore();
10493
+ if (!store) {
10494
+ res.writeHead(503, { "Content-Type": "application/json" });
10495
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10496
+ return;
10497
+ }
10498
+ const { query, limit, threshold, similarity } = parseSearchParams(url);
10499
+ if (query.trim().length === 0) {
10500
+ res.writeHead(400, { "Content-Type": "application/json" });
10501
+ res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
10502
+ return;
10503
+ }
10504
+ try {
10505
+ const hits = await store.search(query, {
10506
+ limit,
10507
+ ...threshold !== void 0 ? { threshold } : {},
10508
+ includeVectors: similarity
10509
+ });
10510
+ const body = {
10511
+ hits: hits.map((h) => ({
10512
+ id: h.entry.id,
10513
+ score: h.score,
10514
+ text: h.entry.text,
10515
+ ...h.entry.summary ? { summary: h.entry.summary } : {},
10516
+ tags: h.entry.tags
10517
+ })),
10518
+ count: hits.length
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
+ }
10526
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10527
+ res.end(JSON.stringify(body));
10528
+ } catch (error2) {
10529
+ res.writeHead(500, { "Content-Type": "application/json" });
10530
+ res.end(
10531
+ JSON.stringify({
10532
+ error: "Vector memory search failed",
10533
+ detail: sanitizeApiError2(error2)
10534
+ })
10535
+ );
10536
+ }
10537
+ }
10538
+ function parseStoreBody(req) {
10539
+ return new Promise((resolve20) => {
10540
+ let raw = "";
10541
+ req.setEncoding("utf8");
10542
+ req.on("data", (chunk) => {
10543
+ raw += chunk;
10544
+ if (raw.length > 64 * 1024) {
10545
+ req.destroy();
10546
+ resolve20(null);
10547
+ }
10548
+ });
10549
+ req.on("end", () => {
10550
+ try {
10551
+ resolve20(raw ? JSON.parse(raw) : {});
10552
+ } catch {
10553
+ resolve20(null);
10554
+ }
10555
+ });
10556
+ req.on("error", () => resolve20(null));
10557
+ });
10558
+ }
10559
+ async function handleVectorMemoryStore(res, req, getStore) {
10560
+ const store = getStore();
10561
+ if (!store) {
10562
+ res.writeHead(503, { "Content-Type": "application/json" });
10563
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10564
+ return;
10565
+ }
10566
+ const body = await parseStoreBody(req);
10567
+ if (!body) {
10568
+ res.writeHead(400, { "Content-Type": "application/json" });
10569
+ res.end(JSON.stringify({ error: "Malformed JSON body" }));
10570
+ return;
10571
+ }
10572
+ const text2 = typeof body.text === "string" ? body.text.trim() : "";
10573
+ if (text2.length === 0) {
10574
+ res.writeHead(400, { "Content-Type": "application/json" });
10575
+ res.end(JSON.stringify({ error: "Missing required field `text`" }));
10576
+ return;
10577
+ }
10578
+ const tags = Array.isArray(body.tags) ? body.tags.filter((t) => typeof t === "string") : [];
10579
+ try {
10580
+ const entry = await store.remember({ text: text2, tags });
10581
+ res.writeHead(200, { "Content-Type": "application/json" });
10582
+ res.end(
10583
+ JSON.stringify({
10584
+ id: entry.id,
10585
+ hasVector: entry.vector !== void 0,
10586
+ dimensions: entry.dimensions
10587
+ })
10588
+ );
10589
+ } catch (error2) {
10590
+ res.writeHead(500, { "Content-Type": "application/json" });
10591
+ res.end(
10592
+ JSON.stringify({
10593
+ error: "Vector memory store failed",
10594
+ detail: sanitizeApiError2(error2)
10595
+ })
10596
+ );
10597
+ }
10598
+ }
10599
+ async function handleVectorMemoryForget(res, url, getStore) {
10600
+ const store = getStore();
10601
+ if (!store) {
10602
+ res.writeHead(503, { "Content-Type": "application/json" });
10603
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10604
+ return;
10605
+ }
10606
+ const match = /^\/api\/vector-memory\/store\/([^/]+)$/.exec(url.pathname);
10607
+ const id = match ? strictDecodeParam(decodeSessionId(match[1]), res) : null;
10608
+ if (id === null) return;
10609
+ try {
10610
+ const removed = await store.forget(id);
10611
+ res.writeHead(200, { "Content-Type": "application/json" });
10612
+ res.end(JSON.stringify({ removed }));
10613
+ } catch (error2) {
10614
+ res.writeHead(500, { "Content-Type": "application/json" });
10615
+ res.end(
10616
+ JSON.stringify({
10617
+ error: "Vector memory forget failed",
10618
+ detail: sanitizeApiError2(error2)
10619
+ })
10620
+ );
10621
+ }
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
+ }
10703
+
10413
10704
  // src/server/http-server/api-router.ts
10414
10705
  async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessTokenOk, getTechStackRuntime) {
10415
10706
  if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
@@ -10748,7 +11039,7 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
10748
11039
  res.end(
10749
11040
  JSON.stringify({
10750
11041
  error: "TechStack store unavailable",
10751
- detail: sanitizeApiError2(error2)
11042
+ detail: sanitizeApiError3(error2)
10752
11043
  })
10753
11044
  );
10754
11045
  return true;
@@ -10811,6 +11102,63 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
10811
11102
  );
10812
11103
  return true;
10813
11104
  }
11105
+ const vectorForgetMatch = /^\/api\/vector-memory\/store\/([^/]+)$/.exec(url.pathname);
11106
+ if (vectorForgetMatch && req.method === "DELETE") {
11107
+ if (requireAccessToken && !accessTokenOk) {
11108
+ res.writeHead(401, { "Content-Type": "application/json" });
11109
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11110
+ return true;
11111
+ }
11112
+ await handleVectorMemoryForget(
11113
+ res,
11114
+ url,
11115
+ () => deps2.getVectorMemoryStore?.()
11116
+ );
11117
+ return true;
11118
+ }
11119
+ if (url.pathname === "/api/vector-memory/status" && req.method === "GET") {
11120
+ if (requireAccessToken && !accessTokenOk) {
11121
+ res.writeHead(401, { "Content-Type": "application/json" });
11122
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11123
+ return true;
11124
+ }
11125
+ await handleVectorMemoryStatus(
11126
+ res,
11127
+ () => deps2.getVectorMemoryStore?.(),
11128
+ {
11129
+ ...deps2.projectRoot ? { projectRoot: deps2.projectRoot } : {},
11130
+ ...deps2.vectorMemoryModelCacheDir ? { modelCacheDir: deps2.vectorMemoryModelCacheDir } : {}
11131
+ }
11132
+ );
11133
+ return true;
11134
+ }
11135
+ if (url.pathname === "/api/vector-memory/search" && req.method === "GET") {
11136
+ if (requireAccessToken && !accessTokenOk) {
11137
+ res.writeHead(401, { "Content-Type": "application/json" });
11138
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11139
+ return true;
11140
+ }
11141
+ await handleVectorMemorySearch(res, url, () => deps2.getVectorMemoryStore?.());
11142
+ return true;
11143
+ }
11144
+ if (url.pathname === "/api/vector-memory/store" && req.method === "POST") {
11145
+ if (requireAccessToken && !accessTokenOk) {
11146
+ res.writeHead(401, { "Content-Type": "application/json" });
11147
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11148
+ return true;
11149
+ }
11150
+ await handleVectorMemoryStore(res, req, () => deps2.getVectorMemoryStore?.());
11151
+ return true;
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
+ }
10814
11162
  if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
10815
11163
  await handleDeadCodeActionPlan(
10816
11164
  res,
@@ -10975,12 +11323,15 @@ function createHttpServer(opts) {
10975
11323
  distDir,
10976
11324
  url,
10977
11325
  opts,
10978
- 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,
10979
11330
  shouldSetAuthCookie
10980
11331
  );
10981
11332
  } catch (err) {
10982
11333
  if (err.code === "ENOENT") {
10983
- await handleSpaFallback(res, distDir, opts, port);
11334
+ await handleSpaFallback(res, distDir, opts, res.socket?.localPort ?? port);
10984
11335
  } else {
10985
11336
  console.error({ url: req.url, err });
10986
11337
  res.writeHead(500);
@@ -13911,13 +14262,13 @@ async function handleMcpRoute(ws, msg, handlers) {
13911
14262
  }
13912
14263
 
13913
14264
  // src/server/memory-handlers.ts
13914
- import { getSageSurface } from "@wrongstack/sage";
14265
+ import { getSageSurface as getSageSurface2 } from "@wrongstack/sage";
13915
14266
  function requiresSage(command) {
13916
14267
  return `\`${command}\` requires the SAGE backend (Sage.enabled).`;
13917
14268
  }
13918
14269
  async function handleMemoryList(ws, memoryStore) {
13919
14270
  try {
13920
- const Sage = getSageSurface(memoryStore);
14271
+ const Sage = getSageSurface2(memoryStore);
13921
14272
  if (Sage) {
13922
14273
  const [stats, memories] = await Promise.all([Sage.stats(), Sage.listSage()]);
13923
14274
  const text3 = memories.length === 0 ? "\u{1F9E0} SAGE is empty." : formatSageText(stats, memories);
@@ -13952,7 +14303,7 @@ function formatSageText(stats, memories) {
13952
14303
  return lines.join("\n");
13953
14304
  }
13954
14305
  async function handleSageList(ws, memoryStore) {
13955
- const Sage = getSageSurface(memoryStore);
14306
+ const Sage = getSageSurface2(memoryStore);
13956
14307
  if (!Sage) {
13957
14308
  send(ws, {
13958
14309
  type: "memory.sage.list",
@@ -13968,7 +14319,7 @@ async function handleSageList(ws, memoryStore) {
13968
14319
  }
13969
14320
  }
13970
14321
  async function handleSageListPage(ws, msg, memoryStore) {
13971
- const Sage = getSageSurface(memoryStore);
14322
+ const Sage = getSageSurface2(memoryStore);
13972
14323
  if (!Sage) {
13973
14324
  send(ws, {
13974
14325
  type: "memory.sage.listPage",
@@ -14018,8 +14369,50 @@ async function handleSageListPage(ws, msg, memoryStore) {
14018
14369
  send(ws, { type: "memory.sage.listPage", payload: { error: errMessage(err) } });
14019
14370
  }
14020
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
+ }
14021
14414
  async function handleSageGet(ws, msg, memoryStore) {
14022
- const Sage = getSageSurface(memoryStore);
14415
+ const Sage = getSageSurface2(memoryStore);
14023
14416
  if (!Sage) {
14024
14417
  send(ws, {
14025
14418
  type: "memory.sage.get",
@@ -14044,7 +14437,7 @@ async function handleSageGet(ws, msg, memoryStore) {
14044
14437
  }
14045
14438
  }
14046
14439
  async function handleSageGraph(ws, msg, memoryStore) {
14047
- const Sage = getSageSurface(memoryStore);
14440
+ const Sage = getSageSurface2(memoryStore);
14048
14441
  if (!Sage?.graphFor) {
14049
14442
  send(ws, {
14050
14443
  type: "memory.sage.graph",
@@ -14078,7 +14471,7 @@ async function handleSageGraph(ws, msg, memoryStore) {
14078
14471
  }
14079
14472
  }
14080
14473
  async function handleSageUpdate(ws, msg, memoryStore) {
14081
- const Sage = getSageSurface(memoryStore);
14474
+ const Sage = getSageSurface2(memoryStore);
14082
14475
  if (!Sage) {
14083
14476
  send(ws, {
14084
14477
  type: "memory.sage.update",
@@ -14106,7 +14499,7 @@ async function handleSageUpdate(ws, msg, memoryStore) {
14106
14499
  }
14107
14500
  }
14108
14501
  async function handleSageRemember(ws, msg, memoryStore) {
14109
- const Sage = getSageSurface(memoryStore);
14502
+ const Sage = getSageSurface2(memoryStore);
14110
14503
  if (!Sage) {
14111
14504
  send(ws, {
14112
14505
  type: "memory.sage.remember",
@@ -14140,7 +14533,7 @@ async function handleSageRemember(ws, msg, memoryStore) {
14140
14533
  }
14141
14534
  }
14142
14535
  async function handleSageDelete(ws, msg, memoryStore) {
14143
- const Sage = getSageSurface(memoryStore);
14536
+ const Sage = getSageSurface2(memoryStore);
14144
14537
  if (!Sage) {
14145
14538
  send(ws, {
14146
14539
  type: "memory.sage.delete",
@@ -14173,7 +14566,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
14173
14566
  }
14174
14567
  }
14175
14568
  async function handleSageRecover(ws, msg, memoryStore) {
14176
- const Sage = getSageSurface(memoryStore);
14569
+ const Sage = getSageSurface2(memoryStore);
14177
14570
  if (!Sage?.recoverSage) {
14178
14571
  send(ws, {
14179
14572
  type: "memory.sage.recover",
@@ -14217,7 +14610,7 @@ async function handleSageRecover(ws, msg, memoryStore) {
14217
14610
  }
14218
14611
  }
14219
14612
  async function handleSageListCandidates(ws, msg, memoryStore) {
14220
- const Sage = getSageSurface(memoryStore);
14613
+ const Sage = getSageSurface2(memoryStore);
14221
14614
  if (!Sage) {
14222
14615
  send(ws, {
14223
14616
  type: "memory.sage.listCandidates",
@@ -14245,7 +14638,7 @@ async function handleSageListCandidates(ws, msg, memoryStore) {
14245
14638
  }
14246
14639
  }
14247
14640
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
14248
- const Sage = getSageSurface(memoryStore);
14641
+ const Sage = getSageSurface2(memoryStore);
14249
14642
  if (!Sage) {
14250
14643
  send(ws, {
14251
14644
  type: "memory.sage.candidateResolve",
@@ -14302,7 +14695,7 @@ async function handleSageCandidateResolve(ws, msg, memoryStore) {
14302
14695
  }
14303
14696
  }
14304
14697
  async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14305
- const Sage = getSageSurface(memoryStore);
14698
+ const Sage = getSageSurface2(memoryStore);
14306
14699
  if (!Sage?.backfillRecoverable) {
14307
14700
  send(ws, {
14308
14701
  type: "memory.sage.backfillRecoverable",
@@ -14342,7 +14735,7 @@ async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14342
14735
  }
14343
14736
  }
14344
14737
  async function handleSageForFile(ws, msg, memoryStore) {
14345
- const Sage = getSageSurface(memoryStore);
14738
+ const Sage = getSageSurface2(memoryStore);
14346
14739
  if (!Sage?.findMemoriesForFile) {
14347
14740
  send(ws, {
14348
14741
  type: "memory.sage.forFile",
@@ -14434,6 +14827,9 @@ async function handleMemoryRoute(ctx, ws, message) {
14434
14827
  case "memory.sage.forFile":
14435
14828
  await handleSageForFile(ws, message, store);
14436
14829
  return true;
14830
+ case "memory.sage.searchBreakdown":
14831
+ await handleSageSearchBreakdown(ws, message, store);
14832
+ return true;
14437
14833
  default:
14438
14834
  return false;
14439
14835
  }
@@ -14824,23 +15220,31 @@ var SURFACE_DEFAULT_PORTS = {
14824
15220
  webui: { http: 3456 },
14825
15221
  simpleui: { http: 3466 }
14826
15222
  };
15223
+ var MAX_TCP_PORT = 65535;
14827
15224
  function surfaceLabel(surface) {
14828
15225
  return surface === "webui" ? "WebUI" : "SimpleUI";
14829
15226
  }
15227
+ function isStrictPort() {
15228
+ const value = process.env["WEBUI_STRICT_PORT"];
15229
+ return value === "1" || value === "true";
15230
+ }
14830
15231
  function getSurfaceDefaultPorts(surface) {
14831
15232
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
14832
15233
  }
14833
15234
  function isPortFree(host, port) {
15235
+ return probePort(host, port).then((err) => err === null);
15236
+ }
15237
+ function probePort(host, port) {
14834
15238
  return new Promise((resolve20) => {
14835
15239
  const srv = net2.createServer();
14836
- srv.once("error", () => resolve20(false));
15240
+ srv.once("error", (err) => resolve20(err));
14837
15241
  srv.once("listening", () => {
14838
- srv.close(() => resolve20(true));
15242
+ srv.close(() => resolve20(null));
14839
15243
  });
14840
15244
  try {
14841
15245
  srv.listen(port, host);
14842
- } catch {
14843
- resolve20(false);
15246
+ } catch (err) {
15247
+ resolve20(err);
14844
15248
  }
14845
15249
  });
14846
15250
  }
@@ -14849,7 +15253,7 @@ async function findFreePort(host, startPort, opts = {}) {
14849
15253
  const maxTries = opts.maxTries ?? 200;
14850
15254
  let port = startPort;
14851
15255
  for (let i = 0; i < maxTries; i++) {
14852
- if (port > 65535) port = 1024 + port % 5e4;
15256
+ if (port > MAX_TCP_PORT) port = 1024 + port % 5e4;
14853
15257
  if (!exclude.has(port) && await isPortFree(host, port)) {
14854
15258
  return port;
14855
15259
  }
@@ -14860,6 +15264,50 @@ async function findFreePort(host, startPort, opts = {}) {
14860
15264
  field: "port"
14861
15265
  });
14862
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
+ }
14863
15311
 
14864
15312
  // src/server/frontend-static-serve.ts
14865
15313
  import { spawn as spawn2 } from "node:child_process";
@@ -14988,10 +15436,15 @@ async function startStaticServe(opts, deps2 = {}) {
14988
15436
  apiToken: opts.apiToken,
14989
15437
  requireToken: opts.requireToken,
14990
15438
  allowedHostnames: opts.allowedHostnames,
14991
- intakeService
15439
+ intakeService,
15440
+ ...opts.getVectorMemoryStore ? { getVectorMemoryStore: opts.getVectorMemoryStore } : {},
15441
+ ...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
14992
15442
  });
14993
15443
  if (!opts.deferListen) {
14994
- 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 };
14995
15448
  }
14996
15449
  return { server, port: opts.httpPort };
14997
15450
  }
@@ -15124,7 +15577,7 @@ function announceWebuiReady(p) {
15124
15577
  token: p.wsToken,
15125
15578
  publicUrl: p.publicUrl
15126
15579
  });
15127
- p.server.on("listening", () => {
15580
+ const announce = () => {
15128
15581
  const extraUrls = formatExternalAccessUrls({
15129
15582
  bindHost: p.host,
15130
15583
  port: p.httpPort,
@@ -15141,7 +15594,12 @@ ${extraUrls.join("\n")}
15141
15594
  ${extraBlock}`
15142
15595
  );
15143
15596
  if (p.open) launch(openUrl);
15144
- });
15597
+ };
15598
+ if (p.server.listening) {
15599
+ announce();
15600
+ return;
15601
+ }
15602
+ p.server.on("listening", announce);
15145
15603
  }
15146
15604
  var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
15147
15605
  async function runBounded(work, timeoutMs, label, debug) {
@@ -18232,6 +18690,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
18232
18690
  "memory.sage.listPage",
18233
18691
  "memory.sage.recover",
18234
18692
  "memory.sage.remember",
18693
+ "memory.sage.searchBreakdown",
18235
18694
  "memory.sage.update"
18236
18695
  ];
18237
18696
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -18519,6 +18978,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
18519
18978
  "memory.sage.listPage",
18520
18979
  "memory.sage.recover",
18521
18980
  "memory.sage.remember",
18981
+ "memory.sage.searchBreakdown",
18522
18982
  "memory.sage.update"
18523
18983
  ];
18524
18984
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -19396,7 +19856,12 @@ function createSessionHandlers(ctx) {
19396
19856
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
19397
19857
  );
19398
19858
  ctx.tokenCounter.reset?.();
19399
- 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
+ }
19400
19865
  ctx.setSessionStartedAt?.(Date.now());
19401
19866
  await ctx.onSessionSwapped?.(next.id);
19402
19867
  };
@@ -19429,6 +19894,10 @@ function createSessionHandlers(ctx) {
19429
19894
  return;
19430
19895
  }
19431
19896
  } else {
19897
+ try {
19898
+ ctx.abortActiveRun?.(clearedSessionId);
19899
+ } catch {
19900
+ }
19432
19901
  ctx.context.state.replaceMessages([]);
19433
19902
  ctx.context.state.replaceTodos([]);
19434
19903
  resetContextAccounting();
@@ -21424,7 +21893,7 @@ function createEmbeddedMessageRouter(deps2) {
21424
21893
  // background after session.new/resume. The run's own end() cleanup
21425
21894
  // removes controllers from the map when it unwinds.
21426
21895
  abortActiveRun: (sessionId) => {
21427
- if (sessionId) {
21896
+ if (sessionId && deps2.conversationCtx.abortControllers.has(sessionId)) {
21428
21897
  deps2.conversationCtx.abortControllers.get(sessionId)?.abort();
21429
21898
  } else {
21430
21899
  for (const controller of [...deps2.conversationCtx.abortControllers.values()]) {
@@ -24203,6 +24672,11 @@ import {
24203
24672
  wstackGlobalRoot as wstackGlobalRoot4
24204
24673
  } from "@wrongstack/core/utils";
24205
24674
  import { ensureSessionShell } from "@wrongstack/tools";
24675
+ import {
24676
+ TransformersEmbeddingProvider,
24677
+ VectorMemoryStore,
24678
+ startFirstBootSageSync
24679
+ } from "@wrongstack/vector-memory";
24206
24680
 
24207
24681
  // src/server/backend-services.ts
24208
24682
  import { join as join15 } from "node:path";
@@ -27155,7 +27629,7 @@ async function resolvePorts(opts) {
27155
27629
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
27156
27630
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
27157
27631
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
27158
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
27632
+ const strictPort = isStrictPort();
27159
27633
  let httpPort = requestedHttpPort;
27160
27634
  if (!strictPort) {
27161
27635
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -27356,7 +27830,9 @@ function startHttpServer(opts) {
27356
27830
  getLlm: opts.getLlm,
27357
27831
  executePackageOperation: opts.executePackageOperation,
27358
27832
  projectRoot: opts.projectRoot,
27359
- intakeService
27833
+ intakeService,
27834
+ ...opts.getVectorMemoryStore ? { getVectorMemoryStore: opts.getVectorMemoryStore } : {},
27835
+ ...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
27360
27836
  });
27361
27837
  return httpServer;
27362
27838
  }
@@ -27372,7 +27848,7 @@ function registerShutdown(deps2) {
27372
27848
 
27373
27849
  // src/server/start-webui-companion.ts
27374
27850
  import * as http2 from "node:http";
27375
- function setupCompanionServer(httpServer, wsHost, httpPort) {
27851
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
27376
27852
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
27377
27853
  if (!companion) return null;
27378
27854
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -27383,16 +27859,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
27383
27859
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
27384
27860
  );
27385
27861
  companionServer.on("error", (err) => {
27386
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
27862
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
27387
27863
  if (!expected) {
27388
27864
  console.warn(
27389
27865
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
27390
27866
  );
27391
27867
  }
27392
27868
  });
27393
- companionServer.listen(httpPort, companion, () => {
27394
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
27395
- });
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}`);
27396
27879
  return companionServer;
27397
27880
  }
27398
27881
 
@@ -27587,6 +28070,7 @@ function setupWebuiShutdown(options) {
27587
28070
  await options.memoryStore.dispose().catch(
27588
28071
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage16(err)}`)
27589
28072
  );
28073
+ options.vectorMemoryStore?.close();
27590
28074
  await unregisterInstance(process.pid, path37.dirname(options.globalConfigPath));
27591
28075
  }
27592
28076
  });
@@ -27651,7 +28135,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
27651
28135
  async function startWebUI(opts = {}) {
27652
28136
  ensureSessionShell();
27653
28137
  const ports = await resolvePorts(opts);
27654
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
28138
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
28139
+ let httpPort = ports.httpPort;
27655
28140
  console.log("[WebUI] Starting backend services...");
27656
28141
  const boot = await bootConfig();
27657
28142
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -27679,6 +28164,27 @@ async function startWebUI(opts = {}) {
27679
28164
  );
27680
28165
  }
27681
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
+ }
27682
28188
  const preContext = await createPreContextServices({
27683
28189
  config,
27684
28190
  wpaths,
@@ -27726,6 +28232,13 @@ async function startWebUI(opts = {}) {
27726
28232
  let sessionStartedAt = preContext.sessionStartedAt;
27727
28233
  let modeId = preContext.modeId;
27728
28234
  const needsSetup = preContext.needsSetup;
28235
+ if (vectorMemoryStore) {
28236
+ void startFirstBootSageSync({
28237
+ store: vectorMemoryStore,
28238
+ memoryStore,
28239
+ logger
28240
+ });
28241
+ }
27729
28242
  const prefSnapshot2 = () => prefSnapshot(context.meta);
27730
28243
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
27731
28244
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -27846,7 +28359,12 @@ async function startWebUI(opts = {}) {
27846
28359
  events,
27847
28360
  permissionPolicy
27848
28361
  }),
27849
- 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
27850
28368
  });
27851
28369
  const wsResult = createWsServers(httpServer, ports, accessToken);
27852
28370
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -27901,7 +28419,25 @@ async function startWebUI(opts = {}) {
27901
28419
  },
27902
28420
  watcherMetricsRef
27903
28421
  );
27904
- 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
+ {
27905
28441
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
27906
28442
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
27907
28443
  const extraUrls = formatExternalAccessUrls({
@@ -27912,8 +28448,8 @@ async function startWebUI(opts = {}) {
27912
28448
  if (extraUrls.length > 0) {
27913
28449
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
27914
28450
  }
27915
- });
27916
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
28451
+ }
28452
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
27917
28453
  async function touchProjectEntry(root, workDir) {
27918
28454
  const resolved = path38.resolve(root);
27919
28455
  const manifest = await loadManifest(globalConfigPath);
@@ -28172,6 +28708,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
28172
28708
  },
28173
28709
  codebaseIndexing,
28174
28710
  memoryStore,
28711
+ vectorMemoryStore,
28175
28712
  globalConfigPath
28176
28713
  });
28177
28714
  }
@@ -28405,9 +28942,11 @@ export {
28405
28942
  isPidAlive,
28406
28943
  isPortFree,
28407
28944
  isRegisteredMessageType,
28945
+ isStrictPort,
28408
28946
  isWildcardBind,
28409
28947
  joinSessionRegistryWithWebUIInstances,
28410
28948
  listInstances,
28949
+ listenWithRetry,
28411
28950
  loadManifest,
28412
28951
  loadSavedProviders,
28413
28952
  markConnectionActivity,