@wrongstack/webui-server 0.308.0 → 0.308.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {
@@ -15811,9 +16042,19 @@ async function handleBrainAsk(ctx, ws, question) {
15811
16042
  risk: "medium",
15812
16043
  fallback: "ask_human"
15813
16044
  });
16045
+ const answerSessionId = ctx.getSessionId?.();
15814
16046
  ctx.send(ws, {
15815
16047
  type: "brain.answer",
15816
- payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
16048
+ // Omit sessionId when there is no session: this is a direct reply to
16049
+ // the asker, not a broadcast, but the client's session gate
16050
+ // (isActiveSessionMessage) is fail-closed on a present-but-empty
16051
+ // sessionId — stamping '' would hide the answer from its own asker
16052
+ // in an embedded host with an unbound agent context.
16053
+ payload: {
16054
+ ...answerSessionId ? { sessionId: answerSessionId } : {},
16055
+ question: q,
16056
+ decision
16057
+ }
15817
16058
  });
15818
16059
  } catch (err) {
15819
16060
  sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
@@ -18299,7 +18540,12 @@ function createProviderHandlers(deps2) {
18299
18540
 
18300
18541
  // src/server/session-handlers.ts
18301
18542
  import { loadTodosCheckpoint } from "@wrongstack/core/storage";
18302
- import { DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveContextWindowPolicy } from "@wrongstack/core/types";
18543
+ import {
18544
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY,
18545
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
18546
+ isContextWindowModeId,
18547
+ resolveContextWindowPolicy
18548
+ } from "@wrongstack/core/types";
18303
18549
  import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
18304
18550
 
18305
18551
  // src/protocol/connection-fsm.ts
@@ -18459,6 +18705,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
18459
18705
  "memory.sage.listPage",
18460
18706
  "memory.sage.recover",
18461
18707
  "memory.sage.remember",
18708
+ "memory.sage.searchBreakdown",
18462
18709
  "memory.sage.update"
18463
18710
  ];
18464
18711
  var CLIENT_EXTENSION_MESSAGE_TYPES = [
@@ -18746,6 +18993,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
18746
18993
  "memory.sage.listPage",
18747
18994
  "memory.sage.recover",
18748
18995
  "memory.sage.remember",
18996
+ "memory.sage.searchBreakdown",
18749
18997
  "memory.sage.update"
18750
18998
  ];
18751
18999
  var SERVER_EXTENSION_MESSAGE_TYPES = [
@@ -18882,6 +19130,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
18882
19130
  var SERVER_CONFIGURATION_MESSAGE_TYPES = [
18883
19131
  "auth.oauth.status",
18884
19132
  "codebase.index.server.shutdown_result",
19133
+ "connections.auto_heal_status",
18885
19134
  "connections.health_error",
18886
19135
  "connections.health_result",
18887
19136
  "connections.service_action_result",
@@ -19623,7 +19872,12 @@ function createSessionHandlers(ctx) {
19623
19872
  sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
19624
19873
  );
19625
19874
  ctx.tokenCounter.reset?.();
19626
- if (usage) ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
19875
+ if (usage) {
19876
+ ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
19877
+ if (typeof usage.input === "number" && usage.input > 0) {
19878
+ ctx.context.lastRequestTokens = usage.input;
19879
+ }
19880
+ }
19627
19881
  ctx.setSessionStartedAt?.(Date.now());
19628
19882
  await ctx.onSessionSwapped?.(next.id);
19629
19883
  };
@@ -19846,8 +20100,8 @@ function createSessionHandlers(ctx) {
19846
20100
  return;
19847
20101
  }
19848
20102
  const { id } = parsed.value;
19849
- let policy = resolveContextWindowPolicy({}, id);
19850
- if (policy.id !== id) {
20103
+ let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
20104
+ if (!isContextWindowModeId(id) && policy.id !== id) {
19851
20105
  const customModes = (await modeStore()).list().filter((m) => m.custom === true);
19852
20106
  const custom = customModes.find((m) => m.id === id);
19853
20107
  if (!custom) {
@@ -19858,6 +20112,7 @@ function createSessionHandlers(ctx) {
19858
20112
  }
19859
20113
  ctx.context.meta["contextWindowMode"] = policy.id;
19860
20114
  ctx.context.meta["contextWindowPolicy"] = policy;
20115
+ ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
19861
20116
  result(ws, true, `Context mode switched to ${policy.id}`);
19862
20117
  broadcastToAll({
19863
20118
  type: "context.mode.changed",
@@ -19921,11 +20176,14 @@ function createSessionHandlers(ctx) {
19921
20176
  }
19922
20177
  const { id } = parsed.value;
19923
20178
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
19924
- ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
19925
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
20179
+ const policy = resolveContextWindowPolicy(
19926
20180
  {},
19927
- DEFAULT_CONTEXT_WINDOW_MODE_ID
20181
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
20182
+ readSessionWindowTokens(ctx.context)
19928
20183
  );
20184
+ ctx.context.meta["contextWindowMode"] = policy.id;
20185
+ ctx.context.meta["contextWindowPolicy"] = policy;
20186
+ delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
19929
20187
  }
19930
20188
  const store = await modeStore();
19931
20189
  const operation = store.remove(id);
@@ -20132,6 +20390,12 @@ function createSessionHandlers(ctx) {
20132
20390
  }
20133
20391
  };
20134
20392
  }
20393
+ function readSessionWindowTokens(context) {
20394
+ const meta = context.meta?.["effectiveMaxContext"];
20395
+ if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
20396
+ const cap = context.provider?.capabilities?.maxContext;
20397
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
20398
+ }
20135
20399
 
20136
20400
  // src/server/embedded-host-adapters.ts
20137
20401
  async function applyEmbeddedModelSwitch(ctx, providerId, modelId) {
@@ -20794,6 +21058,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
20794
21058
  return true;
20795
21059
  }
20796
21060
 
21061
+ // src/server/connections/auto-healer.ts
21062
+ var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
21063
+ var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
21064
+ var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
21065
+ var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
21066
+ var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
21067
+ "kanban",
21068
+ "sage",
21069
+ "chronicle",
21070
+ "codebase-index",
21071
+ "mailbox"
21072
+ ]);
21073
+ function isAutoHealEnabled() {
21074
+ return process.env[AUTO_HEAL_ENV_FLAG] === "1";
21075
+ }
21076
+ function createAutoHealer(options) {
21077
+ const enabled = options.enabled ?? isAutoHealEnabled();
21078
+ const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
21079
+ const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
21080
+ const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
21081
+ const collect = options.collect ?? (() => collectConnectionsHealth({
21082
+ projectRoot: options.projectRoot(),
21083
+ indexDir: options.indexDir(),
21084
+ backend: "standalone"
21085
+ }));
21086
+ const execute = options.execute ?? executeServiceAction;
21087
+ const services = /* @__PURE__ */ new Map();
21088
+ let timer = null;
21089
+ let running = false;
21090
+ let ticking = false;
21091
+ let disposed = false;
21092
+ let inFlightTick = null;
21093
+ let lastTickAt = null;
21094
+ let warnedNoBoundary = false;
21095
+ function stateFor(serviceId) {
21096
+ let state = services.get(serviceId);
21097
+ if (!state) {
21098
+ state = {
21099
+ lastAttemptAt: null,
21100
+ consecutiveFailures: 0,
21101
+ lastSuccess: null,
21102
+ lastMessage: null,
21103
+ inFlight: false,
21104
+ escalated: false
21105
+ };
21106
+ services.set(serviceId, state);
21107
+ }
21108
+ return state;
21109
+ }
21110
+ function snapshot() {
21111
+ return {
21112
+ enabled,
21113
+ running,
21114
+ lastTickAt,
21115
+ services: Object.fromEntries(services)
21116
+ };
21117
+ }
21118
+ function emitStatus(event) {
21119
+ try {
21120
+ options.onStatus?.({ ...event, at: Date.now() });
21121
+ } catch (error2) {
21122
+ options.logger?.warn?.(
21123
+ `[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
21124
+ );
21125
+ }
21126
+ }
21127
+ async function tick() {
21128
+ if (!enabled || disposed) return snapshot();
21129
+ if (!options.trustBoundary) {
21130
+ if (!warnedNoBoundary) {
21131
+ warnedNoBoundary = true;
21132
+ options.logger?.warn?.(
21133
+ "[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
21134
+ );
21135
+ }
21136
+ return snapshot();
21137
+ }
21138
+ if (ticking) return snapshot();
21139
+ ticking = true;
21140
+ try {
21141
+ const report = await collect();
21142
+ const now = Date.now();
21143
+ const projectRoot = options.projectRoot();
21144
+ const indexDir = options.indexDir();
21145
+ for (const service of report.services) {
21146
+ if (disposed) break;
21147
+ const state = stateFor(service.id);
21148
+ if (service.status !== "error") {
21149
+ state.consecutiveFailures = 0;
21150
+ state.escalated = false;
21151
+ continue;
21152
+ }
21153
+ if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
21154
+ continue;
21155
+ }
21156
+ if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
21157
+ if (state.consecutiveFailures >= maxAttempts) {
21158
+ state.escalated = true;
21159
+ options.logger?.warn?.(
21160
+ `[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
21161
+ );
21162
+ continue;
21163
+ }
21164
+ if (state.inFlight) continue;
21165
+ const authorization = await authorizeWebUIAction(
21166
+ options.trustBoundary,
21167
+ {
21168
+ capability: "connections.service.restart",
21169
+ subject: { kind: "process", id: `${service.id}@${projectRoot}` },
21170
+ risk: "elevated",
21171
+ cwd: projectRoot,
21172
+ metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
21173
+ },
21174
+ options.logger
21175
+ );
21176
+ if (disposed) break;
21177
+ if (!authorization.allowed) {
21178
+ state.lastAttemptAt = now;
21179
+ state.lastMessage = `refused by policy: ${authorization.reason}`;
21180
+ emitStatus({
21181
+ serviceId: service.id,
21182
+ phase: "refused",
21183
+ message: `refused by policy: ${authorization.reason}`,
21184
+ attempt: state.consecutiveFailures + 1
21185
+ });
21186
+ options.logger?.warn?.(
21187
+ `[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
21188
+ );
21189
+ continue;
21190
+ }
21191
+ state.inFlight = true;
21192
+ const attempt = state.consecutiveFailures + 1;
21193
+ emitStatus({
21194
+ serviceId: service.id,
21195
+ phase: "restarting",
21196
+ message: `Auto-restarting ${service.id}`,
21197
+ attempt
21198
+ });
21199
+ try {
21200
+ const result = await execute(service.id, "restart", projectRoot, indexDir);
21201
+ state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
21202
+ state.lastSuccess = result.success;
21203
+ state.lastMessage = result.message;
21204
+ emitStatus({
21205
+ serviceId: service.id,
21206
+ phase: result.success ? "restarted" : "failed",
21207
+ message: result.message,
21208
+ attempt
21209
+ });
21210
+ if (!result.success && state.consecutiveFailures >= maxAttempts) {
21211
+ state.escalated = true;
21212
+ emitStatus({
21213
+ serviceId: service.id,
21214
+ phase: "escalated",
21215
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
21216
+ attempt
21217
+ });
21218
+ options.logger?.warn?.(
21219
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
21220
+ );
21221
+ }
21222
+ options.logger?.[result.success ? "info" : "warn"]?.(
21223
+ `[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
21224
+ );
21225
+ } catch (error2) {
21226
+ state.consecutiveFailures += 1;
21227
+ state.lastSuccess = false;
21228
+ state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
21229
+ emitStatus({
21230
+ serviceId: service.id,
21231
+ phase: "failed",
21232
+ message: state.lastMessage,
21233
+ attempt
21234
+ });
21235
+ if (state.consecutiveFailures >= maxAttempts) {
21236
+ state.escalated = true;
21237
+ emitStatus({
21238
+ serviceId: service.id,
21239
+ phase: "escalated",
21240
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
21241
+ attempt
21242
+ });
21243
+ options.logger?.warn?.(
21244
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
21245
+ );
21246
+ }
21247
+ options.logger?.warn?.(
21248
+ `[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
21249
+ );
21250
+ } finally {
21251
+ state.lastAttemptAt = Date.now();
21252
+ state.inFlight = false;
21253
+ }
21254
+ }
21255
+ lastTickAt = Date.now();
21256
+ } catch (error2) {
21257
+ options.logger?.warn?.(
21258
+ `[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
21259
+ );
21260
+ } finally {
21261
+ ticking = false;
21262
+ }
21263
+ return snapshot();
21264
+ }
21265
+ function runTick() {
21266
+ if (!enabled || disposed || running === false || ticking) return;
21267
+ const pending = tick();
21268
+ const tracked = pending.then(
21269
+ () => void 0,
21270
+ () => void 0
21271
+ );
21272
+ inFlightTick = tracked;
21273
+ void tracked.finally(() => {
21274
+ if (inFlightTick === tracked) inFlightTick = null;
21275
+ });
21276
+ }
21277
+ function stopInternal() {
21278
+ if (timer) {
21279
+ clearInterval(timer);
21280
+ timer = null;
21281
+ }
21282
+ running = false;
21283
+ }
21284
+ return {
21285
+ start() {
21286
+ if (!enabled || running || disposed) return;
21287
+ running = true;
21288
+ runTick();
21289
+ timer = setInterval(runTick, intervalMs);
21290
+ timer.unref?.();
21291
+ },
21292
+ stop: stopInternal,
21293
+ async dispose() {
21294
+ stopInternal();
21295
+ disposed = true;
21296
+ const pending = inFlightTick;
21297
+ if (pending) {
21298
+ await Promise.race([
21299
+ pending,
21300
+ new Promise((resolve20) => {
21301
+ const t = setTimeout(resolve20, 3e4);
21302
+ t.unref?.();
21303
+ })
21304
+ ]);
21305
+ }
21306
+ disposed = true;
21307
+ },
21308
+ tick,
21309
+ getSnapshot: snapshot,
21310
+ isRunning: () => running
21311
+ };
21312
+ }
21313
+
20797
21314
  // src/server/fallback-choice.ts
20798
21315
  function emitFallbackChoice(events, msg) {
20799
21316
  const parsed = validateModelFallbackChoicePayload(msg.payload);
@@ -21483,6 +22000,22 @@ async function handleShellOpen(req, logger, options) {
21483
22000
  function createEmbeddedMessageRouter(deps2) {
21484
22001
  const { opts, send: send2, sendResult: sendResult7 } = deps2;
21485
22002
  const projectRoot = () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
22003
+ const autoHealer = createAutoHealer({
22004
+ projectRoot,
22005
+ indexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
22006
+ trustBoundary: deps2.trustBoundary,
22007
+ logger: deps2.logger,
22008
+ onStatus: (event) => deps2.providerCtx.broadcast({
22009
+ type: "connections.auto_heal_status",
22010
+ payload: event
22011
+ })
22012
+ });
22013
+ autoHealer.start();
22014
+ if (deps2.onDispose) {
22015
+ deps2.onDispose(async () => {
22016
+ await autoHealer.dispose();
22017
+ });
22018
+ }
21486
22019
  const terminal = async (ws, message) => {
21487
22020
  await deps2.terminalHandler.handleMessage(ws, message).catch((error2) => {
21488
22021
  const text2 = error2 instanceof Error ? error2.message : String(error2);
@@ -24434,6 +24967,11 @@ import {
24434
24967
  wstackGlobalRoot as wstackGlobalRoot4
24435
24968
  } from "@wrongstack/core/utils";
24436
24969
  import { ensureSessionShell } from "@wrongstack/tools";
24970
+ import {
24971
+ TransformersEmbeddingProvider,
24972
+ VectorMemoryStore,
24973
+ startFirstBootSageSync
24974
+ } from "@wrongstack/vector-memory";
24437
24975
 
24438
24976
  // src/server/backend-services.ts
24439
24977
  import { join as join15 } from "node:path";
@@ -24461,6 +24999,7 @@ import {
24461
24999
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
24462
25000
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
24463
25001
  import {
25002
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
24464
25003
  DEFAULT_TOOLS_CONFIG,
24465
25004
  resolveContextWindowPolicy as resolveContextWindowPolicy2
24466
25005
  } from "@wrongstack/core/types";
@@ -25381,22 +25920,26 @@ async function createAgentServices(input) {
25381
25920
  summarizerModel: config.context?.summarizerModel,
25382
25921
  llmSelector: config.context?.llmSelector
25383
25922
  });
25384
- const initialContextPolicy = resolveContextWindowPolicy2(config.context);
25923
+ let effectiveMaxContext = 0;
25924
+ try {
25925
+ const m = await resolveProviderModelMetadata(
25926
+ modelsRegistry,
25927
+ config.provider,
25928
+ context.model,
25929
+ config.providers?.[config.provider]
25930
+ );
25931
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
25932
+ } catch {
25933
+ }
25934
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
25935
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
25936
+ const initialContextPolicy = resolveContextWindowPolicy2(
25937
+ config.context,
25938
+ void 0,
25939
+ effectiveMaxContext
25940
+ );
25385
25941
  let autoCompactor;
25386
25942
  if (config.context?.autoCompact !== false) {
25387
- let effectiveMaxContext = 0;
25388
- try {
25389
- const m = await resolveProviderModelMetadata(
25390
- modelsRegistry,
25391
- config.provider,
25392
- context.model,
25393
- config.providers?.[config.provider]
25394
- );
25395
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
25396
- } catch {
25397
- }
25398
- if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
25399
- if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
25400
25943
  autoCompactor = new AutoCompactionMiddlewareCtor(
25401
25944
  compactor,
25402
25945
  effectiveMaxContext,
@@ -25451,6 +25994,15 @@ async function createAgentServices(input) {
25451
25994
  context.meta["effectiveMaxContext"] = newMaxContext;
25452
25995
  autoCompactor?.setMaxContext(newMaxContext);
25453
25996
  autoCompactor?.setEnabled(config.context?.autoCompact !== false);
25997
+ if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
25998
+ const policy = resolveContextWindowPolicy2(
25999
+ currentConfig.context ?? {},
26000
+ void 0,
26001
+ newMaxContext
26002
+ );
26003
+ context.meta["contextWindowMode"] = policy.id;
26004
+ context.meta["contextWindowPolicy"] = policy;
26005
+ }
25454
26006
  } else {
25455
26007
  delete context.meta["effectiveMaxContext"];
25456
26008
  autoCompactor?.setEnabled(false);
@@ -26082,8 +26634,22 @@ function createMessageDispatcher(opts) {
26082
26634
  broadcast: (message) => broadcast(state.getClients(), message),
26083
26635
  log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
26084
26636
  });
26637
+ const autoHealer = createAutoHealer({
26638
+ projectRoot: () => state.getProjectRoot(),
26639
+ indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
26640
+ trustBoundary: deps2.trustBoundary,
26641
+ logger: deps2.logger,
26642
+ onStatus: (event) => broadcast(state.getClients(), {
26643
+ type: "connections.auto_heal_status",
26644
+ payload: event
26645
+ })
26646
+ });
26647
+ autoHealer.start();
26085
26648
  if (opts.onDispose) {
26086
- const dispose = () => kanbanSupervisor.dispose();
26649
+ const dispose = async () => {
26650
+ kanbanSupervisor.dispose();
26651
+ await autoHealer.dispose();
26652
+ };
26087
26653
  opts.onDispose(dispose);
26088
26654
  }
26089
26655
  const kanbanContext = () => ({
@@ -26947,7 +27513,11 @@ async function createPreContextServices(input) {
26947
27513
  model: config.model
26948
27514
  });
26949
27515
  context.meta["promptOnlineAgents"] = onlineAgents;
26950
- const initialContextPolicy = resolveContextWindowPolicy3(config.context);
27516
+ const initialContextPolicy = resolveContextWindowPolicy3(
27517
+ config.context,
27518
+ void 0,
27519
+ provider.capabilities?.maxContext
27520
+ );
26951
27521
  context.meta["contextWindowMode"] = initialContextPolicy.id;
26952
27522
  context.meta["contextWindowPolicy"] = initialContextPolicy;
26953
27523
  context.state.setMeta(
@@ -27386,7 +27956,7 @@ async function resolvePorts(opts) {
27386
27956
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
27387
27957
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
27388
27958
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
27389
- const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
27959
+ const strictPort = isStrictPort();
27390
27960
  let httpPort = requestedHttpPort;
27391
27961
  if (!strictPort) {
27392
27962
  httpPort = await findFreePort(wsHost, requestedHttpPort);
@@ -27605,7 +28175,7 @@ function registerShutdown(deps2) {
27605
28175
 
27606
28176
  // src/server/start-webui-companion.ts
27607
28177
  import * as http2 from "node:http";
27608
- function setupCompanionServer(httpServer, wsHost, httpPort) {
28178
+ async function setupCompanionServer(httpServer, wsHost, httpPort) {
27609
28179
  const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
27610
28180
  if (!companion) return null;
27611
28181
  const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
@@ -27616,16 +28186,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
27616
28186
  (req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
27617
28187
  );
27618
28188
  companionServer.on("error", (err) => {
27619
- const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
28189
+ const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
27620
28190
  if (!expected) {
27621
28191
  console.warn(
27622
28192
  `[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
27623
28193
  );
27624
28194
  }
27625
28195
  });
27626
- companionServer.listen(httpPort, companion, () => {
27627
- console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
27628
- });
28196
+ try {
28197
+ await listenWithRetry(companionServer, companion, httpPort, { maxTries: 1 });
28198
+ } catch (err) {
28199
+ const code = err?.code ?? "unknown";
28200
+ console.warn(
28201
+ `[WebUI] companion listener on ${companionLabel} not started (${code}): ${err?.message ?? err}. The primary address is unaffected.`
28202
+ );
28203
+ return null;
28204
+ }
28205
+ console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
27629
28206
  return companionServer;
27630
28207
  }
27631
28208
 
@@ -27785,7 +28362,7 @@ function setupWebuiShutdown(options) {
27785
28362
  onPreShutdown: async () => {
27786
28363
  await options.stopEmptySessionCleanup.dispose();
27787
28364
  const disposeKanban = options.getKanbanSupervisorDispose();
27788
- disposeKanban?.();
28365
+ await disposeKanban?.();
27789
28366
  },
27790
28367
  onShutdown: async () => {
27791
28368
  unregister();
@@ -27820,6 +28397,7 @@ function setupWebuiShutdown(options) {
27820
28397
  await options.memoryStore.dispose().catch(
27821
28398
  (err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage16(err)}`)
27822
28399
  );
28400
+ options.vectorMemoryStore?.close();
27823
28401
  await unregisterInstance(process.pid, path37.dirname(options.globalConfigPath));
27824
28402
  }
27825
28403
  });
@@ -27884,7 +28462,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
27884
28462
  async function startWebUI(opts = {}) {
27885
28463
  ensureSessionShell();
27886
28464
  const ports = await resolvePorts(opts);
27887
- const { wsHost, httpPort, publicUrl, publicWsUrl, requireToken } = ports;
28465
+ const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
28466
+ let httpPort = ports.httpPort;
27888
28467
  console.log("[WebUI] Starting backend services...");
27889
28468
  const boot = await bootConfig();
27890
28469
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
@@ -27912,6 +28491,27 @@ async function startWebUI(opts = {}) {
27912
28491
  );
27913
28492
  }
27914
28493
  const needsProvider = !config.provider || !config.model;
28494
+ let vectorMemoryStore;
28495
+ const vectorMemoryModelCacheDir = path38.join(
28496
+ projectRoot,
28497
+ ".wrongstack",
28498
+ "cache",
28499
+ "transformers-models"
28500
+ );
28501
+ try {
28502
+ vectorMemoryStore = new VectorMemoryStore({
28503
+ provider: new TransformersEmbeddingProvider({
28504
+ cacheDir: vectorMemoryModelCacheDir
28505
+ }),
28506
+ projectRoot
28507
+ });
28508
+ } catch (error2) {
28509
+ const message = error2 instanceof Error ? error2.message : String(error2);
28510
+ logger.warn(
28511
+ `vector memory store disabled: ${message} \u2014 standalone WebUI will run on the SAGE-only surface.`
28512
+ );
28513
+ vectorMemoryStore = void 0;
28514
+ }
27915
28515
  const preContext = await createPreContextServices({
27916
28516
  config,
27917
28517
  wpaths,
@@ -27959,6 +28559,13 @@ async function startWebUI(opts = {}) {
27959
28559
  let sessionStartedAt = preContext.sessionStartedAt;
27960
28560
  let modeId = preContext.modeId;
27961
28561
  const needsSetup = preContext.needsSetup;
28562
+ if (vectorMemoryStore) {
28563
+ void startFirstBootSageSync({
28564
+ store: vectorMemoryStore,
28565
+ memoryStore,
28566
+ logger
28567
+ });
28568
+ }
27962
28569
  const prefSnapshot2 = () => prefSnapshot(context.meta);
27963
28570
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
27964
28571
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
@@ -28079,7 +28686,12 @@ async function startWebUI(opts = {}) {
28079
28686
  events,
28080
28687
  permissionPolicy
28081
28688
  }),
28082
- distDir: opts.distDir
28689
+ distDir: opts.distDir,
28690
+ // Vector memory store — mirrors the CLI host. When `vectorMemoryStore`
28691
+ // construction failed (read-only FS, etc.) we still pass the getter;
28692
+ // it just resolves to `undefined` and the API router answers 503.
28693
+ getVectorMemoryStore: () => vectorMemoryStore,
28694
+ vectorMemoryModelCacheDir
28083
28695
  });
28084
28696
  const wsResult = createWsServers(httpServer, ports, accessToken);
28085
28697
  const { wssPrimary, wssSecondary, clients } = wsResult;
@@ -28134,7 +28746,25 @@ async function startWebUI(opts = {}) {
28134
28746
  },
28135
28747
  watcherMetricsRef
28136
28748
  );
28137
- httpServer.listen(httpPort, wsHost, () => {
28749
+ const strictPort = isStrictPort();
28750
+ const boundPort = await listenWithRetry(httpServer, wsHost, httpPort, {
28751
+ maxTries: strictPort ? 1 : 10
28752
+ });
28753
+ if (boundPort !== httpPort) {
28754
+ console.warn(
28755
+ JSON.stringify({
28756
+ level: "warn",
28757
+ event: "webui.port_reassigned",
28758
+ protocol: "HTTP",
28759
+ requested: httpPort,
28760
+ assigned: boundPort,
28761
+ reason: "bind-time EADDRINUSE retry",
28762
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
28763
+ })
28764
+ );
28765
+ httpPort = boundPort;
28766
+ }
28767
+ {
28138
28768
  const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
28139
28769
  console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
28140
28770
  const extraUrls = formatExternalAccessUrls({
@@ -28145,8 +28775,8 @@ async function startWebUI(opts = {}) {
28145
28775
  if (extraUrls.length > 0) {
28146
28776
  console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
28147
28777
  }
28148
- });
28149
- const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
28778
+ }
28779
+ const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
28150
28780
  async function touchProjectEntry(root, workDir) {
28151
28781
  const resolved = path38.resolve(root);
28152
28782
  const manifest = await loadManifest(globalConfigPath);
@@ -28405,6 +29035,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
28405
29035
  },
28406
29036
  codebaseIndexing,
28407
29037
  memoryStore,
29038
+ vectorMemoryStore,
28408
29039
  globalConfigPath
28409
29040
  });
28410
29041
  }
@@ -28638,9 +29269,11 @@ export {
28638
29269
  isPidAlive,
28639
29270
  isPortFree,
28640
29271
  isRegisteredMessageType,
29272
+ isStrictPort,
28641
29273
  isWildcardBind,
28642
29274
  joinSessionRegistryWithWebUIInstances,
28643
29275
  listInstances,
29276
+ listenWithRetry,
28644
29277
  loadManifest,
28645
29278
  loadSavedProviders,
28646
29279
  markConnectionActivity,