@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.
@@ -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 });
@@ -8889,7 +8889,7 @@ function verifyClient(input) {
8889
8889
 
8890
8890
  // src/server/http-server/api-router.ts
8891
8891
  import * as v8 from "node:v8";
8892
- import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
8892
+ import { sanitizeApiError as sanitizeApiError3 } from "@wrongstack/core/security";
8893
8893
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
8894
8894
 
8895
8895
  // src/server/codemap-handlers.ts
@@ -10316,6 +10316,297 @@ function strictDecodeParam(segment, res) {
10316
10316
  }
10317
10317
  }
10318
10318
 
10319
+ // src/server/http-server/vector-memory-handlers.ts
10320
+ import { getSageSurface } from "@wrongstack/sage";
10321
+ import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
10322
+ function snapshotVectorMemory(store, opts = {}) {
10323
+ const stats = store.stats();
10324
+ return {
10325
+ storePath: opts.projectRoot,
10326
+ modelCacheDir: opts.modelCacheDir,
10327
+ stats
10328
+ };
10329
+ }
10330
+ function snapshotVectorMemoryCache(store) {
10331
+ return store.cacheStats();
10332
+ }
10333
+ async function handleVectorMemoryStatus(res, getStore, opts = {}) {
10334
+ const store = getStore();
10335
+ if (!store) {
10336
+ const body = { enabled: false };
10337
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10338
+ res.end(JSON.stringify(body));
10339
+ return;
10340
+ }
10341
+ try {
10342
+ const snap = snapshotVectorMemory(store, opts);
10343
+ const body = {
10344
+ enabled: true,
10345
+ storePath: snap.storePath,
10346
+ modelCacheDir: snap.modelCacheDir,
10347
+ providerId: snap.stats.modelId,
10348
+ modelId: snap.stats.modelId,
10349
+ dimensions: snap.stats.dimensions,
10350
+ entries: snap.stats.entries,
10351
+ vectors: snap.stats.vectors,
10352
+ providers: snap.stats.providers,
10353
+ cache: snapshotVectorMemoryCache(store)
10354
+ };
10355
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10356
+ res.end(JSON.stringify(body));
10357
+ } catch (error2) {
10358
+ res.writeHead(500, { "Content-Type": "application/json" });
10359
+ res.end(
10360
+ JSON.stringify({
10361
+ error: "Vector memory status failed",
10362
+ detail: sanitizeApiError2(error2)
10363
+ })
10364
+ );
10365
+ }
10366
+ }
10367
+ function parseSearchParams(url) {
10368
+ const query = url.searchParams.get("q") ?? "";
10369
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "10", 10);
10370
+ const limit = Math.min(50, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10));
10371
+ const rawThreshold = url.searchParams.get("threshold");
10372
+ const threshold = rawThreshold !== null && rawThreshold !== "" ? Math.max(0, Math.min(1, Number.parseFloat(rawThreshold))) : 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;
10396
+ }
10397
+ async function handleVectorMemorySearch(res, url, getStore) {
10398
+ const store = getStore();
10399
+ if (!store) {
10400
+ res.writeHead(503, { "Content-Type": "application/json" });
10401
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10402
+ return;
10403
+ }
10404
+ const { query, limit, threshold, similarity } = parseSearchParams(url);
10405
+ if (query.trim().length === 0) {
10406
+ res.writeHead(400, { "Content-Type": "application/json" });
10407
+ res.end(JSON.stringify({ error: "Missing required query parameter `q`" }));
10408
+ return;
10409
+ }
10410
+ try {
10411
+ const hits = await store.search(query, {
10412
+ limit,
10413
+ ...threshold !== void 0 ? { threshold } : {},
10414
+ includeVectors: similarity
10415
+ });
10416
+ const body = {
10417
+ hits: hits.map((h) => ({
10418
+ id: h.entry.id,
10419
+ score: h.score,
10420
+ text: h.entry.text,
10421
+ ...h.entry.summary ? { summary: h.entry.summary } : {},
10422
+ tags: h.entry.tags
10423
+ })),
10424
+ count: hits.length
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
+ }
10432
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10433
+ res.end(JSON.stringify(body));
10434
+ } catch (error2) {
10435
+ res.writeHead(500, { "Content-Type": "application/json" });
10436
+ res.end(
10437
+ JSON.stringify({
10438
+ error: "Vector memory search failed",
10439
+ detail: sanitizeApiError2(error2)
10440
+ })
10441
+ );
10442
+ }
10443
+ }
10444
+ function parseStoreBody(req) {
10445
+ return new Promise((resolve19) => {
10446
+ let raw = "";
10447
+ req.setEncoding("utf8");
10448
+ req.on("data", (chunk) => {
10449
+ raw += chunk;
10450
+ if (raw.length > 64 * 1024) {
10451
+ req.destroy();
10452
+ resolve19(null);
10453
+ }
10454
+ });
10455
+ req.on("end", () => {
10456
+ try {
10457
+ resolve19(raw ? JSON.parse(raw) : {});
10458
+ } catch {
10459
+ resolve19(null);
10460
+ }
10461
+ });
10462
+ req.on("error", () => resolve19(null));
10463
+ });
10464
+ }
10465
+ async function handleVectorMemoryStore(res, req, getStore) {
10466
+ const store = getStore();
10467
+ if (!store) {
10468
+ res.writeHead(503, { "Content-Type": "application/json" });
10469
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10470
+ return;
10471
+ }
10472
+ const body = await parseStoreBody(req);
10473
+ if (!body) {
10474
+ res.writeHead(400, { "Content-Type": "application/json" });
10475
+ res.end(JSON.stringify({ error: "Malformed JSON body" }));
10476
+ return;
10477
+ }
10478
+ const text = typeof body.text === "string" ? body.text.trim() : "";
10479
+ if (text.length === 0) {
10480
+ res.writeHead(400, { "Content-Type": "application/json" });
10481
+ res.end(JSON.stringify({ error: "Missing required field `text`" }));
10482
+ return;
10483
+ }
10484
+ const tags = Array.isArray(body.tags) ? body.tags.filter((t) => typeof t === "string") : [];
10485
+ try {
10486
+ const entry = await store.remember({ text, tags });
10487
+ res.writeHead(200, { "Content-Type": "application/json" });
10488
+ res.end(
10489
+ JSON.stringify({
10490
+ id: entry.id,
10491
+ hasVector: entry.vector !== void 0,
10492
+ dimensions: entry.dimensions
10493
+ })
10494
+ );
10495
+ } catch (error2) {
10496
+ res.writeHead(500, { "Content-Type": "application/json" });
10497
+ res.end(
10498
+ JSON.stringify({
10499
+ error: "Vector memory store failed",
10500
+ detail: sanitizeApiError2(error2)
10501
+ })
10502
+ );
10503
+ }
10504
+ }
10505
+ async function handleVectorMemoryForget(res, url, getStore) {
10506
+ const store = getStore();
10507
+ if (!store) {
10508
+ res.writeHead(503, { "Content-Type": "application/json" });
10509
+ res.end(JSON.stringify({ error: "Vector memory not enabled in this host" }));
10510
+ return;
10511
+ }
10512
+ const match = /^\/api\/vector-memory\/store\/([^/]+)$/.exec(url.pathname);
10513
+ const id = match ? strictDecodeParam(decodeSessionId(match[1]), res) : null;
10514
+ if (id === null) return;
10515
+ try {
10516
+ const removed = await store.forget(id);
10517
+ res.writeHead(200, { "Content-Type": "application/json" });
10518
+ res.end(JSON.stringify({ removed }));
10519
+ } catch (error2) {
10520
+ res.writeHead(500, { "Content-Type": "application/json" });
10521
+ res.end(
10522
+ JSON.stringify({
10523
+ error: "Vector memory forget failed",
10524
+ detail: sanitizeApiError2(error2)
10525
+ })
10526
+ );
10527
+ }
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
+ }
10609
+
10319
10610
  // src/server/http-server/api-router.ts
10320
10611
  async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessTokenOk, getTechStackRuntime) {
10321
10612
  if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
@@ -10654,7 +10945,7 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
10654
10945
  res.end(
10655
10946
  JSON.stringify({
10656
10947
  error: "TechStack store unavailable",
10657
- detail: sanitizeApiError2(error2)
10948
+ detail: sanitizeApiError3(error2)
10658
10949
  })
10659
10950
  );
10660
10951
  return true;
@@ -10717,6 +11008,63 @@ async function handleApiRoutes(req, res, url, deps2, requireAccessToken, accessT
10717
11008
  );
10718
11009
  return true;
10719
11010
  }
11011
+ const vectorForgetMatch = /^\/api\/vector-memory\/store\/([^/]+)$/.exec(url.pathname);
11012
+ if (vectorForgetMatch && req.method === "DELETE") {
11013
+ if (requireAccessToken && !accessTokenOk) {
11014
+ res.writeHead(401, { "Content-Type": "application/json" });
11015
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11016
+ return true;
11017
+ }
11018
+ await handleVectorMemoryForget(
11019
+ res,
11020
+ url,
11021
+ () => deps2.getVectorMemoryStore?.()
11022
+ );
11023
+ return true;
11024
+ }
11025
+ if (url.pathname === "/api/vector-memory/status" && req.method === "GET") {
11026
+ if (requireAccessToken && !accessTokenOk) {
11027
+ res.writeHead(401, { "Content-Type": "application/json" });
11028
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11029
+ return true;
11030
+ }
11031
+ await handleVectorMemoryStatus(
11032
+ res,
11033
+ () => deps2.getVectorMemoryStore?.(),
11034
+ {
11035
+ ...deps2.projectRoot ? { projectRoot: deps2.projectRoot } : {},
11036
+ ...deps2.vectorMemoryModelCacheDir ? { modelCacheDir: deps2.vectorMemoryModelCacheDir } : {}
11037
+ }
11038
+ );
11039
+ return true;
11040
+ }
11041
+ if (url.pathname === "/api/vector-memory/search" && req.method === "GET") {
11042
+ if (requireAccessToken && !accessTokenOk) {
11043
+ res.writeHead(401, { "Content-Type": "application/json" });
11044
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11045
+ return true;
11046
+ }
11047
+ await handleVectorMemorySearch(res, url, () => deps2.getVectorMemoryStore?.());
11048
+ return true;
11049
+ }
11050
+ if (url.pathname === "/api/vector-memory/store" && req.method === "POST") {
11051
+ if (requireAccessToken && !accessTokenOk) {
11052
+ res.writeHead(401, { "Content-Type": "application/json" });
11053
+ res.end(JSON.stringify({ error: "Unauthorized" }));
11054
+ return true;
11055
+ }
11056
+ await handleVectorMemoryStore(res, req, () => deps2.getVectorMemoryStore?.());
11057
+ return true;
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
+ }
10720
11068
  if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
10721
11069
  await handleDeadCodeActionPlan(
10722
11070
  res,
@@ -10881,12 +11229,15 @@ function createHttpServer(opts) {
10881
11229
  distDir,
10882
11230
  url,
10883
11231
  opts,
10884
- 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,
10885
11236
  shouldSetAuthCookie
10886
11237
  );
10887
11238
  } catch (err) {
10888
11239
  if (err.code === "ENOENT") {
10889
- await handleSpaFallback(res, distDir, opts, port);
11240
+ await handleSpaFallback(res, distDir, opts, res.socket?.localPort ?? port);
10890
11241
  } else {
10891
11242
  console.error({ url: req.url, err });
10892
11243
  res.writeHead(500);
@@ -13671,13 +14022,13 @@ async function handleMcpRoute(ws, msg, handlers) {
13671
14022
  }
13672
14023
 
13673
14024
  // src/server/memory-handlers.ts
13674
- import { getSageSurface } from "@wrongstack/sage";
14025
+ import { getSageSurface as getSageSurface2 } from "@wrongstack/sage";
13675
14026
  function requiresSage(command) {
13676
14027
  return `\`${command}\` requires the SAGE backend (Sage.enabled).`;
13677
14028
  }
13678
14029
  async function handleMemoryList(ws, memoryStore) {
13679
14030
  try {
13680
- const Sage = getSageSurface(memoryStore);
14031
+ const Sage = getSageSurface2(memoryStore);
13681
14032
  if (Sage) {
13682
14033
  const [stats, memories] = await Promise.all([Sage.stats(), Sage.listSage()]);
13683
14034
  const text2 = memories.length === 0 ? "\u{1F9E0} SAGE is empty." : formatSageText(stats, memories);
@@ -13712,7 +14063,7 @@ function formatSageText(stats, memories) {
13712
14063
  return lines.join("\n");
13713
14064
  }
13714
14065
  async function handleSageList(ws, memoryStore) {
13715
- const Sage = getSageSurface(memoryStore);
14066
+ const Sage = getSageSurface2(memoryStore);
13716
14067
  if (!Sage) {
13717
14068
  send(ws, {
13718
14069
  type: "memory.sage.list",
@@ -13728,7 +14079,7 @@ async function handleSageList(ws, memoryStore) {
13728
14079
  }
13729
14080
  }
13730
14081
  async function handleSageListPage(ws, msg, memoryStore) {
13731
- const Sage = getSageSurface(memoryStore);
14082
+ const Sage = getSageSurface2(memoryStore);
13732
14083
  if (!Sage) {
13733
14084
  send(ws, {
13734
14085
  type: "memory.sage.listPage",
@@ -13778,8 +14129,50 @@ async function handleSageListPage(ws, msg, memoryStore) {
13778
14129
  send(ws, { type: "memory.sage.listPage", payload: { error: errMessage(err) } });
13779
14130
  }
13780
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
+ }
13781
14174
  async function handleSageGet(ws, msg, memoryStore) {
13782
- const Sage = getSageSurface(memoryStore);
14175
+ const Sage = getSageSurface2(memoryStore);
13783
14176
  if (!Sage) {
13784
14177
  send(ws, {
13785
14178
  type: "memory.sage.get",
@@ -13804,7 +14197,7 @@ async function handleSageGet(ws, msg, memoryStore) {
13804
14197
  }
13805
14198
  }
13806
14199
  async function handleSageGraph(ws, msg, memoryStore) {
13807
- const Sage = getSageSurface(memoryStore);
14200
+ const Sage = getSageSurface2(memoryStore);
13808
14201
  if (!Sage?.graphFor) {
13809
14202
  send(ws, {
13810
14203
  type: "memory.sage.graph",
@@ -13838,7 +14231,7 @@ async function handleSageGraph(ws, msg, memoryStore) {
13838
14231
  }
13839
14232
  }
13840
14233
  async function handleSageUpdate(ws, msg, memoryStore) {
13841
- const Sage = getSageSurface(memoryStore);
14234
+ const Sage = getSageSurface2(memoryStore);
13842
14235
  if (!Sage) {
13843
14236
  send(ws, {
13844
14237
  type: "memory.sage.update",
@@ -13866,7 +14259,7 @@ async function handleSageUpdate(ws, msg, memoryStore) {
13866
14259
  }
13867
14260
  }
13868
14261
  async function handleSageRemember(ws, msg, memoryStore) {
13869
- const Sage = getSageSurface(memoryStore);
14262
+ const Sage = getSageSurface2(memoryStore);
13870
14263
  if (!Sage) {
13871
14264
  send(ws, {
13872
14265
  type: "memory.sage.remember",
@@ -13900,7 +14293,7 @@ async function handleSageRemember(ws, msg, memoryStore) {
13900
14293
  }
13901
14294
  }
13902
14295
  async function handleSageDelete(ws, msg, memoryStore) {
13903
- const Sage = getSageSurface(memoryStore);
14296
+ const Sage = getSageSurface2(memoryStore);
13904
14297
  if (!Sage) {
13905
14298
  send(ws, {
13906
14299
  type: "memory.sage.delete",
@@ -13933,7 +14326,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
13933
14326
  }
13934
14327
  }
13935
14328
  async function handleSageRecover(ws, msg, memoryStore) {
13936
- const Sage = getSageSurface(memoryStore);
14329
+ const Sage = getSageSurface2(memoryStore);
13937
14330
  if (!Sage?.recoverSage) {
13938
14331
  send(ws, {
13939
14332
  type: "memory.sage.recover",
@@ -13977,7 +14370,7 @@ async function handleSageRecover(ws, msg, memoryStore) {
13977
14370
  }
13978
14371
  }
13979
14372
  async function handleSageListCandidates(ws, msg, memoryStore) {
13980
- const Sage = getSageSurface(memoryStore);
14373
+ const Sage = getSageSurface2(memoryStore);
13981
14374
  if (!Sage) {
13982
14375
  send(ws, {
13983
14376
  type: "memory.sage.listCandidates",
@@ -14005,7 +14398,7 @@ async function handleSageListCandidates(ws, msg, memoryStore) {
14005
14398
  }
14006
14399
  }
14007
14400
  async function handleSageCandidateResolve(ws, msg, memoryStore) {
14008
- const Sage = getSageSurface(memoryStore);
14401
+ const Sage = getSageSurface2(memoryStore);
14009
14402
  if (!Sage) {
14010
14403
  send(ws, {
14011
14404
  type: "memory.sage.candidateResolve",
@@ -14062,7 +14455,7 @@ async function handleSageCandidateResolve(ws, msg, memoryStore) {
14062
14455
  }
14063
14456
  }
14064
14457
  async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14065
- const Sage = getSageSurface(memoryStore);
14458
+ const Sage = getSageSurface2(memoryStore);
14066
14459
  if (!Sage?.backfillRecoverable) {
14067
14460
  send(ws, {
14068
14461
  type: "memory.sage.backfillRecoverable",
@@ -14102,7 +14495,7 @@ async function handleSageBackfillRecoverable(ws, msg, memoryStore) {
14102
14495
  }
14103
14496
  }
14104
14497
  async function handleSageForFile(ws, msg, memoryStore) {
14105
- const Sage = getSageSurface(memoryStore);
14498
+ const Sage = getSageSurface2(memoryStore);
14106
14499
  if (!Sage?.findMemoriesForFile) {
14107
14500
  send(ws, {
14108
14501
  type: "memory.sage.forFile",
@@ -14194,6 +14587,9 @@ async function handleMemoryRoute(ctx, ws, message) {
14194
14587
  case "memory.sage.forFile":
14195
14588
  await handleSageForFile(ws, message, store);
14196
14589
  return true;
14590
+ case "memory.sage.searchBreakdown":
14591
+ await handleSageSearchBreakdown(ws, message, store);
14592
+ return true;
14197
14593
  default:
14198
14594
  return false;
14199
14595
  }
@@ -14537,17 +14933,25 @@ function createModelOperations(context) {
14537
14933
  // src/server/port-utils.ts
14538
14934
  import * as net2 from "node:net";
14539
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
+ }
14540
14941
  function isPortFree(host, port) {
14942
+ return probePort(host, port).then((err) => err === null);
14943
+ }
14944
+ function probePort(host, port) {
14541
14945
  return new Promise((resolve19) => {
14542
14946
  const srv = net2.createServer();
14543
- srv.once("error", () => resolve19(false));
14947
+ srv.once("error", (err) => resolve19(err));
14544
14948
  srv.once("listening", () => {
14545
- srv.close(() => resolve19(true));
14949
+ srv.close(() => resolve19(null));
14546
14950
  });
14547
14951
  try {
14548
14952
  srv.listen(port, host);
14549
- } catch {
14550
- resolve19(false);
14953
+ } catch (err) {
14954
+ resolve19(err);
14551
14955
  }
14552
14956
  });
14553
14957
  }
@@ -14556,7 +14960,7 @@ async function findFreePort(host, startPort, opts = {}) {
14556
14960
  const maxTries = opts.maxTries ?? 200;
14557
14961
  let port = startPort;
14558
14962
  for (let i = 0; i < maxTries; i++) {
14559
- if (port > 65535) port = 1024 + port % 5e4;
14963
+ if (port > MAX_TCP_PORT) port = 1024 + port % 5e4;
14560
14964
  if (!exclude.has(port) && await isPortFree(host, port)) {
14561
14965
  return port;
14562
14966
  }
@@ -14567,6 +14971,50 @@ async function findFreePort(host, startPort, opts = {}) {
14567
14971
  field: "port"
14568
14972
  });
14569
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
+ }
14570
15018
 
14571
15019
  // src/server/intake-service.ts
14572
15020
  import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
@@ -16952,6 +17400,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
16952
17400
  "memory.sage.listPage",
16953
17401
  "memory.sage.recover",
16954
17402
  "memory.sage.remember",
17403
+ "memory.sage.searchBreakdown",
16955
17404
  "memory.sage.update"
16956
17405
  ];
16957
17406
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -17239,6 +17688,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
17239
17688
  "memory.sage.listPage",
17240
17689
  "memory.sage.recover",
17241
17690
  "memory.sage.remember",
17691
+ "memory.sage.searchBreakdown",
17242
17692
  "memory.sage.update"
17243
17693
  ];
17244
17694
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -17925,7 +18375,12 @@ function createSessionHandlers(ctx) {
17925
18375
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
17926
18376
  );
17927
18377
  ctx.tokenCounter.reset?.();
17928
- 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
+ }
17929
18384
  ctx.setSessionStartedAt?.(Date.now());
17930
18385
  await ctx.onSessionSwapped?.(next.id);
17931
18386
  };
@@ -17958,6 +18413,10 @@ function createSessionHandlers(ctx) {
17958
18413
  return;
17959
18414
  }
17960
18415
  } else {
18416
+ try {
18417
+ ctx.abortActiveRun?.(clearedSessionId);
18418
+ } catch {
18419
+ }
17961
18420
  ctx.context.state.replaceMessages([]);
17962
18421
  ctx.context.state.replaceTodos([]);
17963
18422
  resetContextAccounting();
@@ -22081,6 +22540,11 @@ import {
22081
22540
  wstackGlobalRoot as wstackGlobalRoot2
22082
22541
  } from "@wrongstack/core/utils";
22083
22542
  import { ensureSessionShell } from "@wrongstack/tools";
22543
+ import {
22544
+ TransformersEmbeddingProvider,
22545
+ VectorMemoryStore,
22546
+ startFirstBootSageSync
22547
+ } from "@wrongstack/vector-memory";
22084
22548
 
22085
22549
  // src/server/backend-services.ts
22086
22550
  import { join as join12 } from "node:path";
@@ -25033,7 +25497,7 @@ async function resolvePorts(opts) {
25033
25497
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
25034
25498
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
25035
25499
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
25036
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
25500
+ const strictPort = isStrictPort();
25037
25501
  let httpPort = requestedHttpPort;
25038
25502
  if (!strictPort) {
25039
25503
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -25234,7 +25698,9 @@ function startHttpServer(opts) {
25234
25698
  getLlm: opts.getLlm,
25235
25699
  executePackageOperation: opts.executePackageOperation,
25236
25700
  projectRoot: opts.projectRoot,
25237
- intakeService
25701
+ intakeService,
25702
+ ...opts.getVectorMemoryStore ? { getVectorMemoryStore: opts.getVectorMemoryStore } : {},
25703
+ ...opts.vectorMemoryModelCacheDir ? { vectorMemoryModelCacheDir: opts.vectorMemoryModelCacheDir } : {}
25238
25704
  });
25239
25705
  return httpServer;
25240
25706
  }
@@ -25250,7 +25716,7 @@ function registerShutdown(deps2) {
25250
25716
 
25251
25717
  // src/server/start-webui-companion.ts
25252
25718
  import * as http2 from "node:http";
25253
- function setupCompanionServer(httpServer, wsHost, httpPort) {
25719
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
25254
25720
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
25255
25721
  if (!companion) return null;
25256
25722
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -25261,16 +25727,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
25261
25727
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
25262
25728
  );
25263
25729
  companionServer.on("error", (err) => {
25264
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
25730
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
25265
25731
  if (!expected) {
25266
25732
  console.warn(
25267
25733
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
25268
25734
  );
25269
25735
  }
25270
25736
  });
25271
- companionServer.listen(httpPort, companion, () => {
25272
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
25273
- });
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}`);
25274
25747
  return companionServer;
25275
25748
  }
25276
25749
 
@@ -25465,6 +25938,7 @@ function setupWebuiShutdown(options) {
25465
25938
  await options.memoryStore.dispose().catch(
25466
25939
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage15(err)}`)
25467
25940
  );
25941
+ options.vectorMemoryStore?.close();
25468
25942
  await unregisterInstance(process.pid, path32.dirname(options.globalConfigPath));
25469
25943
  }
25470
25944
  });
@@ -25529,7 +26003,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
25529
26003
  async function startWebUI(opts = {}) {
25530
26004
  ensureSessionShell();
25531
26005
  const ports = await resolvePorts(opts);
25532
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
26006
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
26007
+ let httpPort = ports.httpPort;
25533
26008
  console.log("[WebUI] Starting backend services...");
25534
26009
  const boot = await bootConfig();
25535
26010
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -25557,6 +26032,27 @@ async function startWebUI(opts = {}) {
25557
26032
  );
25558
26033
  }
25559
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
+ }
25560
26056
  const preContext = await createPreContextServices({
25561
26057
  config,
25562
26058
  wpaths,
@@ -25604,6 +26100,13 @@ async function startWebUI(opts = {}) {
25604
26100
  let sessionStartedAt = preContext.sessionStartedAt;
25605
26101
  let modeId = preContext.modeId;
25606
26102
  const needsSetup = preContext.needsSetup;
26103
+ if (vectorMemoryStore) {
26104
+ void startFirstBootSageSync({
26105
+ store: vectorMemoryStore,
26106
+ memoryStore,
26107
+ logger
26108
+ });
26109
+ }
25607
26110
  const prefSnapshot2 = () => prefSnapshot(context.meta);
25608
26111
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
25609
26112
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -25724,7 +26227,12 @@ async function startWebUI(opts = {}) {
25724
26227
  events,
25725
26228
  permissionPolicy
25726
26229
  }),
25727
- 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
25728
26236
  });
25729
26237
  const wsResult = createWsServers(httpServer, ports, accessToken);
25730
26238
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -25779,7 +26287,25 @@ async function startWebUI(opts = {}) {
25779
26287
  },
25780
26288
  watcherMetricsRef
25781
26289
  );
25782
- 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
+ {
25783
26309
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
25784
26310
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
25785
26311
  const extraUrls = formatExternalAccessUrls({
@@ -25790,8 +26316,8 @@ async function startWebUI(opts = {}) {
25790
26316
  if (extraUrls.length > 0) {
25791
26317
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
25792
26318
  }
25793
- });
25794
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
26319
+ }
26320
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
25795
26321
  async function touchProjectEntry(root, workDir) {
25796
26322
  const resolved = path33.resolve(root);
25797
26323
  const manifest = await loadManifest(globalConfigPath);
@@ -26050,6 +26576,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26050
26576
  },
26051
26577
  codebaseIndexing,
26052
26578
  memoryStore,
26579
+ vectorMemoryStore,
26053
26580
  globalConfigPath
26054
26581
  });
26055
26582
  }