@wrongstack/webui-server 0.308.0 → 0.308.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +349 -43
- package/dist/protocol/client-integrations.d.ts +1 -1
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/registry.d.ts +2 -2
- package/dist/protocol/server-integrations.d.ts +1 -1
- package/dist/server/embedded-lifecycle.d.ts +1 -0
- package/dist/server/entry.js +336 -40
- package/dist/server/frontend-static-serve.d.ts +6 -0
- package/dist/server/http-server/api-router.d.ts +7 -0
- package/dist/server/http-server/vector-memory-handlers.d.ts +53 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/memory-handlers.d.ts +19 -0
- package/dist/server/port-utils.d.ts +23 -3
- package/dist/server/start-webui-companion.d.ts +9 -1
- package/dist/server/start-webui-shutdown.d.ts +5 -0
- package/package.json +13 -12
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:
|
|
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 ?
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
15240
|
+
srv.once("error", (err) => resolve20(err));
|
|
15062
15241
|
srv.once("listening", () => {
|
|
15063
|
-
srv.close(() => resolve20(
|
|
15242
|
+
srv.close(() => resolve20(null));
|
|
15064
15243
|
});
|
|
15065
15244
|
try {
|
|
15066
15245
|
srv.listen(port, host);
|
|
15067
|
-
} catch {
|
|
15068
|
-
resolve20(
|
|
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 >
|
|
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.
|
|
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
|
-
|
|
15580
|
+
const announce = () => {
|
|
15355
15581
|
const extraUrls = formatExternalAccessUrls({
|
|
15356
15582
|
bindHost: p.host,
|
|
15357
15583
|
port: p.httpPort,
|
|
@@ -15368,7 +15594,12 @@ ${extraUrls.join("\n")}
|
|
|
15368
15594
|
${extraBlock}`
|
|
15369
15595
|
);
|
|
15370
15596
|
if (p.open) launch(openUrl);
|
|
15371
|
-
}
|
|
15597
|
+
};
|
|
15598
|
+
if (p.server.listening) {
|
|
15599
|
+
announce();
|
|
15600
|
+
return;
|
|
15601
|
+
}
|
|
15602
|
+
p.server.on("listening", announce);
|
|
15372
15603
|
}
|
|
15373
15604
|
var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
|
|
15374
15605
|
async function runBounded(work, timeoutMs, label, debug) {
|
|
@@ -18459,6 +18690,7 @@ var CLIENT_KNOWLEDGE_MESSAGE_TYPES = [
|
|
|
18459
18690
|
"memory.sage.listPage",
|
|
18460
18691
|
"memory.sage.recover",
|
|
18461
18692
|
"memory.sage.remember",
|
|
18693
|
+
"memory.sage.searchBreakdown",
|
|
18462
18694
|
"memory.sage.update"
|
|
18463
18695
|
];
|
|
18464
18696
|
var CLIENT_EXTENSION_MESSAGE_TYPES = [
|
|
@@ -18746,6 +18978,7 @@ var SERVER_KNOWLEDGE_MESSAGE_TYPES = [
|
|
|
18746
18978
|
"memory.sage.listPage",
|
|
18747
18979
|
"memory.sage.recover",
|
|
18748
18980
|
"memory.sage.remember",
|
|
18981
|
+
"memory.sage.searchBreakdown",
|
|
18749
18982
|
"memory.sage.update"
|
|
18750
18983
|
];
|
|
18751
18984
|
var SERVER_EXTENSION_MESSAGE_TYPES = [
|
|
@@ -19623,7 +19856,12 @@ function createSessionHandlers(ctx) {
|
|
|
19623
19856
|
sessionScopedPath(sessionsDirectory(), next.id, ".tasks.json")
|
|
19624
19857
|
);
|
|
19625
19858
|
ctx.tokenCounter.reset?.();
|
|
19626
|
-
if (usage)
|
|
19859
|
+
if (usage) {
|
|
19860
|
+
ctx.tokenCounter.account(usage, currentConfig().model, ctx.context.provider.id);
|
|
19861
|
+
if (typeof usage.input === "number" && usage.input > 0) {
|
|
19862
|
+
ctx.context.lastRequestTokens = usage.input;
|
|
19863
|
+
}
|
|
19864
|
+
}
|
|
19627
19865
|
ctx.setSessionStartedAt?.(Date.now());
|
|
19628
19866
|
await ctx.onSessionSwapped?.(next.id);
|
|
19629
19867
|
};
|
|
@@ -24434,6 +24672,11 @@ import {
|
|
|
24434
24672
|
wstackGlobalRoot as wstackGlobalRoot4
|
|
24435
24673
|
} from "@wrongstack/core/utils";
|
|
24436
24674
|
import { ensureSessionShell } from "@wrongstack/tools";
|
|
24675
|
+
import {
|
|
24676
|
+
TransformersEmbeddingProvider,
|
|
24677
|
+
VectorMemoryStore,
|
|
24678
|
+
startFirstBootSageSync
|
|
24679
|
+
} from "@wrongstack/vector-memory";
|
|
24437
24680
|
|
|
24438
24681
|
// src/server/backend-services.ts
|
|
24439
24682
|
import { join as join15 } from "node:path";
|
|
@@ -27386,7 +27629,7 @@ async function resolvePorts(opts) {
|
|
|
27386
27629
|
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
27387
27630
|
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
27388
27631
|
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
27389
|
-
const strictPort =
|
|
27632
|
+
const strictPort = isStrictPort();
|
|
27390
27633
|
let httpPort = requestedHttpPort;
|
|
27391
27634
|
if (!strictPort) {
|
|
27392
27635
|
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
@@ -27605,7 +27848,7 @@ function registerShutdown(deps2) {
|
|
|
27605
27848
|
|
|
27606
27849
|
// src/server/start-webui-companion.ts
|
|
27607
27850
|
import * as http2 from "node:http";
|
|
27608
|
-
function setupCompanionServer(httpServer, wsHost, httpPort) {
|
|
27851
|
+
async function setupCompanionServer(httpServer, wsHost, httpPort) {
|
|
27609
27852
|
const companion = wsHost === "127.0.0.1" ? "::1" : wsHost === "0.0.0.0" || wsHost === void 0 ? "::" : wsHost === "::" || wsHost === "[::]" ? "0.0.0.0" : null;
|
|
27610
27853
|
if (!companion) return null;
|
|
27611
27854
|
const companionLabel = companion.includes(":") ? `[${companion}]` : companion;
|
|
@@ -27616,16 +27859,23 @@ function setupCompanionServer(httpServer, wsHost, httpPort) {
|
|
|
27616
27859
|
(req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
|
|
27617
27860
|
);
|
|
27618
27861
|
companionServer.on("error", (err) => {
|
|
27619
|
-
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL"
|
|
27862
|
+
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL";
|
|
27620
27863
|
if (!expected) {
|
|
27621
27864
|
console.warn(
|
|
27622
27865
|
`[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
|
|
27623
27866
|
);
|
|
27624
27867
|
}
|
|
27625
27868
|
});
|
|
27626
|
-
|
|
27627
|
-
|
|
27628
|
-
})
|
|
27869
|
+
try {
|
|
27870
|
+
await listenWithRetry(companionServer, companion, httpPort, { maxTries: 1 });
|
|
27871
|
+
} catch (err) {
|
|
27872
|
+
const code = err?.code ?? "unknown";
|
|
27873
|
+
console.warn(
|
|
27874
|
+
`[WebUI] companion listener on ${companionLabel} not started (${code}): ${err?.message ?? err}. The primary address is unaffected.`
|
|
27875
|
+
);
|
|
27876
|
+
return null;
|
|
27877
|
+
}
|
|
27878
|
+
console.log(`[WebUI] HTTP server running on http://${companionLabel}:${httpPort}`);
|
|
27629
27879
|
return companionServer;
|
|
27630
27880
|
}
|
|
27631
27881
|
|
|
@@ -27820,6 +28070,7 @@ function setupWebuiShutdown(options) {
|
|
|
27820
28070
|
await options.memoryStore.dispose().catch(
|
|
27821
28071
|
(err) => options.logger.warn(`sage connection disposal failed: ${toErrorMessage16(err)}`)
|
|
27822
28072
|
);
|
|
28073
|
+
options.vectorMemoryStore?.close();
|
|
27823
28074
|
await unregisterInstance(process.pid, path37.dirname(options.globalConfigPath));
|
|
27824
28075
|
}
|
|
27825
28076
|
});
|
|
@@ -27884,7 +28135,8 @@ function createStandaloneTodosCheckpointLifecycle(input) {
|
|
|
27884
28135
|
async function startWebUI(opts = {}) {
|
|
27885
28136
|
ensureSessionShell();
|
|
27886
28137
|
const ports = await resolvePorts(opts);
|
|
27887
|
-
const { wsHost,
|
|
28138
|
+
const { wsHost, publicUrl, publicWsUrl, requireToken } = ports;
|
|
28139
|
+
let httpPort = ports.httpPort;
|
|
27888
28140
|
console.log("[WebUI] Starting backend services...");
|
|
27889
28141
|
const boot = await bootConfig();
|
|
27890
28142
|
const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
|
|
@@ -27912,6 +28164,27 @@ async function startWebUI(opts = {}) {
|
|
|
27912
28164
|
);
|
|
27913
28165
|
}
|
|
27914
28166
|
const needsProvider = !config.provider || !config.model;
|
|
28167
|
+
let vectorMemoryStore;
|
|
28168
|
+
const vectorMemoryModelCacheDir = path38.join(
|
|
28169
|
+
projectRoot,
|
|
28170
|
+
".wrongstack",
|
|
28171
|
+
"cache",
|
|
28172
|
+
"transformers-models"
|
|
28173
|
+
);
|
|
28174
|
+
try {
|
|
28175
|
+
vectorMemoryStore = new VectorMemoryStore({
|
|
28176
|
+
provider: new TransformersEmbeddingProvider({
|
|
28177
|
+
cacheDir: vectorMemoryModelCacheDir
|
|
28178
|
+
}),
|
|
28179
|
+
projectRoot
|
|
28180
|
+
});
|
|
28181
|
+
} catch (error2) {
|
|
28182
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
28183
|
+
logger.warn(
|
|
28184
|
+
`vector memory store disabled: ${message} \u2014 standalone WebUI will run on the SAGE-only surface.`
|
|
28185
|
+
);
|
|
28186
|
+
vectorMemoryStore = void 0;
|
|
28187
|
+
}
|
|
27915
28188
|
const preContext = await createPreContextServices({
|
|
27916
28189
|
config,
|
|
27917
28190
|
wpaths,
|
|
@@ -27959,6 +28232,13 @@ async function startWebUI(opts = {}) {
|
|
|
27959
28232
|
let sessionStartedAt = preContext.sessionStartedAt;
|
|
27960
28233
|
let modeId = preContext.modeId;
|
|
27961
28234
|
const needsSetup = preContext.needsSetup;
|
|
28235
|
+
if (vectorMemoryStore) {
|
|
28236
|
+
void startFirstBootSageSync({
|
|
28237
|
+
store: vectorMemoryStore,
|
|
28238
|
+
memoryStore,
|
|
28239
|
+
logger
|
|
28240
|
+
});
|
|
28241
|
+
}
|
|
27962
28242
|
const prefSnapshot2 = () => prefSnapshot(context.meta);
|
|
27963
28243
|
const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
|
|
27964
28244
|
const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
|
|
@@ -28079,7 +28359,12 @@ async function startWebUI(opts = {}) {
|
|
|
28079
28359
|
events,
|
|
28080
28360
|
permissionPolicy
|
|
28081
28361
|
}),
|
|
28082
|
-
distDir: opts.distDir
|
|
28362
|
+
distDir: opts.distDir,
|
|
28363
|
+
// Vector memory store — mirrors the CLI host. When `vectorMemoryStore`
|
|
28364
|
+
// construction failed (read-only FS, etc.) we still pass the getter;
|
|
28365
|
+
// it just resolves to `undefined` and the API router answers 503.
|
|
28366
|
+
getVectorMemoryStore: () => vectorMemoryStore,
|
|
28367
|
+
vectorMemoryModelCacheDir
|
|
28083
28368
|
});
|
|
28084
28369
|
const wsResult = createWsServers(httpServer, ports, accessToken);
|
|
28085
28370
|
const { wssPrimary, wssSecondary, clients } = wsResult;
|
|
@@ -28134,7 +28419,25 @@ async function startWebUI(opts = {}) {
|
|
|
28134
28419
|
},
|
|
28135
28420
|
watcherMetricsRef
|
|
28136
28421
|
);
|
|
28137
|
-
|
|
28422
|
+
const strictPort = isStrictPort();
|
|
28423
|
+
const boundPort = await listenWithRetry(httpServer, wsHost, httpPort, {
|
|
28424
|
+
maxTries: strictPort ? 1 : 10
|
|
28425
|
+
});
|
|
28426
|
+
if (boundPort !== httpPort) {
|
|
28427
|
+
console.warn(
|
|
28428
|
+
JSON.stringify({
|
|
28429
|
+
level: "warn",
|
|
28430
|
+
event: "webui.port_reassigned",
|
|
28431
|
+
protocol: "HTTP",
|
|
28432
|
+
requested: httpPort,
|
|
28433
|
+
assigned: boundPort,
|
|
28434
|
+
reason: "bind-time EADDRINUSE retry",
|
|
28435
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
28436
|
+
})
|
|
28437
|
+
);
|
|
28438
|
+
httpPort = boundPort;
|
|
28439
|
+
}
|
|
28440
|
+
{
|
|
28138
28441
|
const authHint = requireToken ? " (authentication required; configure WEBUI_TOKEN out of band)" : "";
|
|
28139
28442
|
console.log(`[WebUI] HTTP server listening on http://${wsHost}:${httpPort}${authHint}`);
|
|
28140
28443
|
const extraUrls = formatExternalAccessUrls({
|
|
@@ -28145,8 +28448,8 @@ async function startWebUI(opts = {}) {
|
|
|
28145
28448
|
if (extraUrls.length > 0) {
|
|
28146
28449
|
console.log("[WebUI] Protected endpoints on external interfaces:\n" + extraUrls.join("\n"));
|
|
28147
28450
|
}
|
|
28148
|
-
}
|
|
28149
|
-
const companionServer = setupCompanionServer(httpServer, wsHost, httpPort);
|
|
28451
|
+
}
|
|
28452
|
+
const companionServer = await setupCompanionServer(httpServer, wsHost, httpPort);
|
|
28150
28453
|
async function touchProjectEntry(root, workDir) {
|
|
28151
28454
|
const resolved = path38.resolve(root);
|
|
28152
28455
|
const manifest = await loadManifest(globalConfigPath);
|
|
@@ -28405,6 +28708,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
28405
28708
|
},
|
|
28406
28709
|
codebaseIndexing,
|
|
28407
28710
|
memoryStore,
|
|
28711
|
+
vectorMemoryStore,
|
|
28408
28712
|
globalConfigPath
|
|
28409
28713
|
});
|
|
28410
28714
|
}
|
|
@@ -28638,9 +28942,11 @@ export {
|
|
|
28638
28942
|
isPidAlive,
|
|
28639
28943
|
isPortFree,
|
|
28640
28944
|
isRegisteredMessageType,
|
|
28945
|
+
isStrictPort,
|
|
28641
28946
|
isWildcardBind,
|
|
28642
28947
|
joinSessionRegistryWithWebUIInstances,
|
|
28643
28948
|
listInstances,
|
|
28949
|
+
listenWithRetry,
|
|
28644
28950
|
loadManifest,
|
|
28645
28951
|
loadSavedProviders,
|
|
28646
28952
|
markConnectionActivity,
|