@polderlabs/bizar 4.5.1 → 4.5.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/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-BRhoDOUz.js} +1 -2
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-BRhoDOUz.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-lbH6szyX.js} +2 -3
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/src/server/headroom.mjs +37 -35
- package/bizar-dash/src/server/memory-lightrag.mjs +39 -31
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/chat.mjs +38 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
- package/bizar-dash/src/server/routes/memory.mjs +12 -4
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +39 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +40 -6
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +1 -1
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Memory.tsx +4 -3
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +4 -2
- package/bizar-dash/src/web/views/Settings.tsx +2 -1
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/cli/artifact.mjs +11 -4
- package/cli/bin.mjs +46 -12
- package/cli/doctor.mjs +5 -3
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +8 -2
- package/install.sh +1 -2
- package/package.json +3 -3
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
- package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
</script>
|
|
32
|
-
<script type="module" crossorigin src="/assets/main-
|
|
33
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
32
|
+
<script type="module" crossorigin src="/assets/main-B4OfGAwz.js"></script>
|
|
33
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-lbH6szyX.js">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
35
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
35
|
+
<link rel="stylesheet" crossorigin href="/assets/main-DAlLdW8I.css">
|
|
36
36
|
</head>
|
|
37
37
|
<body>
|
|
38
38
|
<div id="root"></div>
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
14
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
15
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
15
|
+
<script type="module" crossorigin src="/assets/mobile-BRhoDOUz.js"></script>
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-lbH6szyX.js">
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
@@ -115,42 +115,44 @@ async function findProxyPid(port) {
|
|
|
115
115
|
}
|
|
116
116
|
} catch { /* ignore */ }
|
|
117
117
|
|
|
118
|
-
// Try to find process listening on the port via /proc/net/tcp (Linux)
|
|
118
|
+
// Try to find process listening on the port via /proc/net/tcp (Linux only).
|
|
119
|
+
// On macOS/Windows /proc does not exist — return null early to avoid noisy errors.
|
|
120
|
+
if (process.platform !== 'linux') {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
119
123
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch { /* skip */ }
|
|
153
|
-
}
|
|
124
|
+
const netTcp = readFileSync('/proc/net/tcp', 'utf8');
|
|
125
|
+
const portHex = port.toString(16).toUpperCase().padStart(4, '0');
|
|
126
|
+
const lines = netTcp.split('\n');
|
|
127
|
+
for (const line of lines.slice(1)) {
|
|
128
|
+
// Format: sl local_address rem_address st tx_queue rx_queue ... inode
|
|
129
|
+
const parts = line.trim().split(/\s+/);
|
|
130
|
+
if (parts.length < 10) continue;
|
|
131
|
+
const local = parts[1];
|
|
132
|
+
const localPortHex = local.split(':')[1];
|
|
133
|
+
if (localPortHex?.toUpperCase() === portHex) {
|
|
134
|
+
// Try to get PID from the socket inode
|
|
135
|
+
const inode = parts[9];
|
|
136
|
+
if (inode && inode !== '0') {
|
|
137
|
+
// Scan /proc/*/fd/* for socket inodes
|
|
138
|
+
const { readdirSync, readlinkSync } = await import('node:fs');
|
|
139
|
+
const procDir = '/proc';
|
|
140
|
+
const pids = readdirSync(procDir).filter(
|
|
141
|
+
(n) => /^\d+$/.test(n) && !isNaN(parseInt(n, 10)),
|
|
142
|
+
);
|
|
143
|
+
for (const pid of pids) {
|
|
144
|
+
try {
|
|
145
|
+
const fdDir = join(procDir, pid, 'fd');
|
|
146
|
+
const fds = readdirSync(fdDir);
|
|
147
|
+
for (const fd of fds) {
|
|
148
|
+
try {
|
|
149
|
+
const link = readlinkSync(join(fdDir, fd));
|
|
150
|
+
if (link.includes(`socket:[${inode}]`)) {
|
|
151
|
+
return parseInt(pid, 10);
|
|
152
|
+
}
|
|
153
|
+
} catch { /* skip */ }
|
|
154
|
+
}
|
|
155
|
+
} catch { /* skip */ }
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -84,7 +84,8 @@ function readLogTail(filePath, n = 30) {
|
|
|
84
84
|
const content = readFileSync(filePath, 'utf8');
|
|
85
85
|
const lines = content.split('\n');
|
|
86
86
|
return lines.slice(-n);
|
|
87
|
-
} catch {
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn('[lightrag] swallowed in readLogTail:', err?.message || err);
|
|
88
89
|
return [];
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -206,7 +207,8 @@ export function resolveLightRAGConfig(projectRoot) {
|
|
|
206
207
|
if (existsSync(path)) {
|
|
207
208
|
try {
|
|
208
209
|
userCfg = JSON.parse(readFileSync(path, 'utf8')).lightrag || {};
|
|
209
|
-
} catch {
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.warn('[lightrag] swallowed in resolveLightRAGConfig.readMemoryJson:', err?.message || err);
|
|
210
212
|
userCfg = {};
|
|
211
213
|
}
|
|
212
214
|
}
|
|
@@ -269,8 +271,9 @@ export function findLightragBinary() {
|
|
|
269
271
|
timeout: 3000,
|
|
270
272
|
}).trim();
|
|
271
273
|
if (path) return path;
|
|
272
|
-
} catch {
|
|
274
|
+
} catch (err) {
|
|
273
275
|
// command returned non-zero — fall through
|
|
276
|
+
console.warn('[lightrag] swallowed in findLightragBinary.which:', err?.message || err);
|
|
274
277
|
}
|
|
275
278
|
return null;
|
|
276
279
|
}
|
|
@@ -336,7 +339,8 @@ export async function isRunning(config) {
|
|
|
336
339
|
try {
|
|
337
340
|
const res = await httpGet(`http://${config.host}:${config.port}/health`, 3000);
|
|
338
341
|
return res.status === 200;
|
|
339
|
-
} catch {
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn('[lightrag] swallowed in isRunning.healthCheck:', err?.message || err);
|
|
340
344
|
return false;
|
|
341
345
|
}
|
|
342
346
|
}
|
|
@@ -361,7 +365,8 @@ function readPidAlive(pidFile) {
|
|
|
361
365
|
if (!Number.isFinite(pid) || pid <= 0) return { pid: null, alive: false };
|
|
362
366
|
const alive = killPid(pid, 0); // signal 0 = probe
|
|
363
367
|
return { pid, alive };
|
|
364
|
-
} catch {
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.warn('[lightrag] swallowed in readPidAlive:', err?.message || err);
|
|
365
370
|
return { pid: null, alive: false };
|
|
366
371
|
}
|
|
367
372
|
}
|
|
@@ -387,7 +392,9 @@ export async function startServer(config, { logger } = {}) {
|
|
|
387
392
|
if (existing && !existingAlive) {
|
|
388
393
|
try {
|
|
389
394
|
unlinkSync(pidFile);
|
|
390
|
-
} catch {
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.warn('[lightrag] swallowed in startServer.unlinkStalePid:', err?.message || err);
|
|
397
|
+
}
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
if (!findLightragBinary()) {
|
|
@@ -818,7 +825,8 @@ export async function stats(projectRoot) {
|
|
|
818
825
|
try {
|
|
819
826
|
process.kill(parsed, 0);
|
|
820
827
|
pid = parsed;
|
|
821
|
-
} catch {
|
|
828
|
+
} catch (err) {
|
|
829
|
+
console.warn('[lightrag] swallowed in stats.pidProbe:', err?.message || err);
|
|
822
830
|
pid = null;
|
|
823
831
|
}
|
|
824
832
|
}
|
|
@@ -835,8 +843,8 @@ export async function stats(projectRoot) {
|
|
|
835
843
|
lastReindexOk = m.ok === true;
|
|
836
844
|
lastReindexInserted = typeof m.inserted === 'number' ? m.inserted : null;
|
|
837
845
|
lastReindexFailed = typeof m.failed === 'number' ? m.failed : null;
|
|
838
|
-
} catch {
|
|
839
|
-
|
|
846
|
+
} catch (err) {
|
|
847
|
+
console.warn('[lightrag] swallowed in stats.markerRead:', err?.message || err);
|
|
840
848
|
}
|
|
841
849
|
}
|
|
842
850
|
|
|
@@ -853,13 +861,13 @@ export async function stats(projectRoot) {
|
|
|
853
861
|
try {
|
|
854
862
|
const obj = JSON.parse(readFileSync(p, 'utf8'));
|
|
855
863
|
indexedApprox += Object.keys(obj || {}).length;
|
|
856
|
-
} catch {
|
|
857
|
-
|
|
864
|
+
} catch (err) {
|
|
865
|
+
console.warn('[lightrag] swallowed in stats.kvStoreRead:', err?.message || err);
|
|
858
866
|
}
|
|
859
867
|
}
|
|
860
868
|
}
|
|
861
|
-
} catch {
|
|
862
|
-
|
|
869
|
+
} catch (err) {
|
|
870
|
+
console.warn('[lightrag] swallowed in stats.kvStoreScan:', err?.message || err);
|
|
863
871
|
}
|
|
864
872
|
|
|
865
873
|
// Note count = listNotes from the vault.
|
|
@@ -895,8 +903,8 @@ export async function stats(projectRoot) {
|
|
|
895
903
|
walk(vaultRoot);
|
|
896
904
|
}
|
|
897
905
|
}
|
|
898
|
-
} catch {
|
|
899
|
-
|
|
906
|
+
} catch (err) {
|
|
907
|
+
console.warn('[lightrag] swallowed in stats.noteCount:', err?.message || err);
|
|
900
908
|
}
|
|
901
909
|
|
|
902
910
|
// Query stats: best-effort — LightRAG's /query endpoint doesn't expose
|
|
@@ -920,12 +928,12 @@ export async function stats(projectRoot) {
|
|
|
920
928
|
counted++;
|
|
921
929
|
}
|
|
922
930
|
}
|
|
923
|
-
} catch {
|
|
924
|
-
|
|
931
|
+
} catch (err) {
|
|
932
|
+
console.warn('[lightrag] swallowed in stats.queryLogLine:', err?.message || err);
|
|
925
933
|
}
|
|
926
934
|
}
|
|
927
|
-
} catch {
|
|
928
|
-
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.warn('[lightrag] swallowed in stats.queryLogRead:', err?.message || err);
|
|
929
937
|
}
|
|
930
938
|
}
|
|
931
939
|
const avgResponseMs = counted > 0 ? Math.round(totalResponseMs / counted) : null;
|
|
@@ -974,24 +982,24 @@ export function recordQuery(projectRoot, durationMs) {
|
|
|
974
982
|
const keep = lines.slice(-50000).join('\n');
|
|
975
983
|
writeFileSync(path, keep);
|
|
976
984
|
}
|
|
977
|
-
} catch {
|
|
978
|
-
|
|
985
|
+
} catch (err) {
|
|
986
|
+
console.warn('[lightrag] swallowed in recordQuery.truncate:', err?.message || err);
|
|
979
987
|
}
|
|
980
|
-
} catch {
|
|
981
|
-
|
|
988
|
+
} catch (err) {
|
|
989
|
+
console.warn('[lightrag] swallowed in recordQuery:', err?.message || err);
|
|
982
990
|
}
|
|
983
991
|
}
|
|
984
992
|
|
|
985
993
|
function appendFileSafe(path, content) {
|
|
986
994
|
try {
|
|
987
995
|
appendFileSync(path, content);
|
|
988
|
-
} catch {
|
|
989
|
-
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.warn('[lightrag] appendFileSync failed, falling back to read+write:', err?.message || err);
|
|
990
998
|
try {
|
|
991
999
|
const cur = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
992
1000
|
writeFileSync(path, cur + content);
|
|
993
|
-
} catch {
|
|
994
|
-
|
|
1001
|
+
} catch (err2) {
|
|
1002
|
+
console.warn('[lightrag] swallowed in appendFileSafe.manualWrite:', err2?.message || err2);
|
|
995
1003
|
}
|
|
996
1004
|
}
|
|
997
1005
|
}
|
|
@@ -1024,13 +1032,13 @@ export async function rebuildGraph(projectRoot, opts = {}) {
|
|
|
1024
1032
|
for (const e of readdirSync(config.workingDir)) {
|
|
1025
1033
|
try {
|
|
1026
1034
|
unlinkSync(join(config.workingDir, e));
|
|
1027
|
-
} catch {
|
|
1028
|
-
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
console.warn('[lightrag] swallowed in rebuildGraph.unlinkEntry:', err?.message || err);
|
|
1029
1037
|
}
|
|
1030
1038
|
}
|
|
1031
1039
|
}
|
|
1032
|
-
} catch {
|
|
1033
|
-
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
console.warn('[lightrag] swallowed in rebuildGraph.wipe:', err?.message || err);
|
|
1034
1042
|
}
|
|
1035
1043
|
// Reindex from scratch.
|
|
1036
1044
|
return reindexVault(projectRoot, opts);
|
|
@@ -596,7 +596,8 @@ export const modsLoader = {
|
|
|
596
596
|
const res = await fetch(modJsonUrl);
|
|
597
597
|
if (!res.ok) return null;
|
|
598
598
|
modJson = await res.json();
|
|
599
|
-
} catch {
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.warn('swallowed in fetchModMetadata:', err.message);
|
|
600
601
|
return null;
|
|
601
602
|
}
|
|
602
603
|
const id = modJson.id;
|
|
@@ -674,7 +675,8 @@ export const modsLoader = {
|
|
|
674
675
|
let entries;
|
|
675
676
|
try {
|
|
676
677
|
entries = readdirSync(p, { withFileTypes: true });
|
|
677
|
-
} catch {
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.warn('swallowed in mod walk:', err.message);
|
|
678
680
|
return;
|
|
679
681
|
}
|
|
680
682
|
for (const e of entries) {
|
|
@@ -913,20 +915,26 @@ export const modsLoader = {
|
|
|
913
915
|
try {
|
|
914
916
|
out.agents = readdirSync(OPENCODE_AGENTS_DIR)
|
|
915
917
|
.filter((f) => f.startsWith(prefix));
|
|
916
|
-
} catch {
|
|
918
|
+
} catch (err) {
|
|
919
|
+
console.warn('swallowed in agents readdir:', err.message);
|
|
920
|
+
}
|
|
917
921
|
}
|
|
918
922
|
if (existsSync(OPENCODE_COMMANDS_DIR)) {
|
|
919
923
|
try {
|
|
920
924
|
out.commands = readdirSync(OPENCODE_COMMANDS_DIR)
|
|
921
925
|
.filter((f) => f.startsWith(prefix));
|
|
922
|
-
} catch {
|
|
926
|
+
} catch (err) {
|
|
927
|
+
console.warn('swallowed in commands readdir:', err.message);
|
|
928
|
+
}
|
|
923
929
|
}
|
|
924
930
|
if (existsSync(OPENCODE_SKILLS_DIR)) {
|
|
925
931
|
try {
|
|
926
932
|
out.skills = readdirSync(OPENCODE_SKILLS_DIR, { withFileTypes: true })
|
|
927
933
|
.filter((e) => e.isDirectory() && e.name.startsWith(skillPrefix))
|
|
928
934
|
.map((e) => e.name);
|
|
929
|
-
} catch {
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.warn('swallowed in skills readdir:', err.message);
|
|
937
|
+
}
|
|
930
938
|
}
|
|
931
939
|
return out;
|
|
932
940
|
},
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renameSync,
|
|
39
39
|
mkdirSync,
|
|
40
40
|
readdirSync,
|
|
41
|
+
statSync,
|
|
41
42
|
} from 'node:fs';
|
|
42
43
|
import { dirname, join } from 'node:path';
|
|
43
44
|
import { homedir } from 'node:os';
|
|
@@ -66,13 +67,80 @@ function atomicWriteJson(filePath, data) {
|
|
|
66
67
|
renameSync(tmp, filePath);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// v5.0.0 — Bug S1: 1-second debounced cache for opencode.json reads.
|
|
71
|
+
// `list()`/`listAll()` and other consumers call `loadConfig()` on every
|
|
72
|
+
// WS poll (every ~5s per client) and on every route hit. With N clients
|
|
73
|
+
// that becomes O(N) reads per minute. The cache collapses all reads
|
|
74
|
+
// within a 1-second window to a single read. Writes invalidate so
|
|
75
|
+
// subsequent reads see the new state.
|
|
76
|
+
//
|
|
77
|
+
// The cache lives on `globalThis` so it is shared across all module
|
|
78
|
+
// instances of providers-store.mjs — tests and code that re-imports
|
|
79
|
+
// the module (e.g. via `?cb=` cache-bust) must see the same cache,
|
|
80
|
+
// otherwise a write on one instance becomes invisible to another.
|
|
81
|
+
//
|
|
82
|
+
// The cache also tracks the file's mtime+size and invalidates if the
|
|
83
|
+
// on-disk file has been modified externally (e.g. an editor saving
|
|
84
|
+
// opencode.json, or a test setup writing a fresh file). This prevents
|
|
85
|
+
// stale reads when the file changes outside our write paths.
|
|
86
|
+
const OPENCODE_JSON_CACHE_GLOBAL_KEY = '__bizar_opencode_json_cache__';
|
|
87
|
+
function _opencodeCacheSlot() {
|
|
88
|
+
if (!globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY]) {
|
|
89
|
+
globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY] = { entry: null, at: 0 };
|
|
90
|
+
}
|
|
91
|
+
return globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const OPENCODE_JSON_CACHE_TTL_MS = 1000;
|
|
95
|
+
|
|
96
|
+
function _fileStamp(filePath) {
|
|
97
|
+
// Best-effort stat — if stat fails (file missing, etc.), the caller
|
|
98
|
+
// will re-read and re-populate the cache.
|
|
99
|
+
try {
|
|
100
|
+
const st = statSync(filePath);
|
|
101
|
+
return `${st.size}:${st.mtimeMs}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readOpencodeJsonCached(filePath = OPENCODE_JSON) {
|
|
108
|
+
const slot = _opencodeCacheSlot();
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (slot.entry && slot.entry.filePath === filePath) {
|
|
111
|
+
const age = now - slot.at;
|
|
112
|
+
if (age < OPENCODE_JSON_CACHE_TTL_MS) {
|
|
113
|
+
// Fast path: still within TTL. Verify the file hasn't been
|
|
114
|
+
// modified externally (e.g. another process, an editor, or a
|
|
115
|
+
// test that writes the file directly). The stamp check is cheap
|
|
116
|
+
// and protects correctness when the file is touched outside
|
|
117
|
+
// our write paths.
|
|
118
|
+
const stamp = _fileStamp(filePath);
|
|
119
|
+
if (stamp === null || stamp === slot.entry.stamp) {
|
|
120
|
+
return slot.entry.data;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const data = safeReadJSON(filePath, {});
|
|
125
|
+
slot.entry = { filePath, data, stamp: _fileStamp(filePath) };
|
|
126
|
+
slot.at = now;
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function invalidateOpencodeJsonCache() {
|
|
131
|
+
const slot = _opencodeCacheSlot();
|
|
132
|
+
slot.entry = null;
|
|
133
|
+
slot.at = 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
69
136
|
function loadConfig() {
|
|
70
|
-
return
|
|
137
|
+
return readOpencodeJsonCached(OPENCODE_JSON);
|
|
71
138
|
}
|
|
72
139
|
|
|
73
140
|
function saveConfig(data) {
|
|
74
141
|
mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
|
|
75
142
|
atomicWriteJson(OPENCODE_JSON, data);
|
|
143
|
+
invalidateOpencodeJsonCache();
|
|
76
144
|
}
|
|
77
145
|
|
|
78
146
|
// v4.6.0 — expose the on-disk load/save helpers so route modules can
|
|
@@ -81,6 +149,10 @@ function saveConfig(data) {
|
|
|
81
149
|
// were not exported and config.mjs referenced them implicitly.
|
|
82
150
|
export { loadConfig, saveConfig };
|
|
83
151
|
|
|
152
|
+
// v5.0.0 — expose cache helpers for tests and other consumers that need
|
|
153
|
+
// to force a re-read (e.g. settings-store after a write).
|
|
154
|
+
export { readOpencodeJsonCached, invalidateOpencodeJsonCache, OPENCODE_JSON_CACHE_TTL_MS };
|
|
155
|
+
|
|
84
156
|
// ── v4.6.0 Backup-key rotation constants ────────────────────────────────────
|
|
85
157
|
//
|
|
86
158
|
// ERROR_COOLDOWN_THRESHOLD: after this many recorded errors on the same
|
|
@@ -53,6 +53,36 @@ const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,120}$/;
|
|
|
53
53
|
const MAX_CHAT_SUBSCRIPTIONS = 50;
|
|
54
54
|
let activeChatSubscriptions = 0;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* v5.0.0 — Bug S3: per-chat-session backpressure cap on the SSE→WS
|
|
58
|
+
* forwarding pipeline. The upstream SSE can pump deltas faster than
|
|
59
|
+
* the WS broadcast can flush them if a connected WS client is slow.
|
|
60
|
+
* `safeSend()` in server.mjs already terminates slow WS clients based
|
|
61
|
+
* on the byte-level `bufferedAmount`, but we also need a defensive
|
|
62
|
+
* message-count cap so a single chat session can't accumulate an
|
|
63
|
+
* unbounded number of queued deltas in this process. When a session
|
|
64
|
+
* exceeds CHAT_DELTA_BUFFER_CAP, additional deltas are dropped with
|
|
65
|
+
* a warning log so operators see the issue.
|
|
66
|
+
*/
|
|
67
|
+
const CHAT_DELTA_BUFFER_CAP = 1000;
|
|
68
|
+
const chatDeltaCounts = new Map(); // chatSessionId -> count since idle
|
|
69
|
+
|
|
70
|
+
function noteChatDelta(chatSessionId) {
|
|
71
|
+
const cur = chatDeltaCounts.get(chatSessionId) || 0;
|
|
72
|
+
if (cur >= CHAT_DELTA_BUFFER_CAP) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[chat] dropped delta for session ${chatSessionId}: per-session cap (${CHAT_DELTA_BUFFER_CAP}) exceeded; client is too slow`,
|
|
75
|
+
);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
chatDeltaCounts.set(chatSessionId, cur + 1);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resetChatDeltaCount(chatSessionId) {
|
|
83
|
+
chatDeltaCounts.delete(chatSessionId);
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
/**
|
|
57
87
|
* @param {object} deps
|
|
58
88
|
* @param {object} deps.state
|
|
@@ -489,6 +519,11 @@ async function streamOpencodeSession({
|
|
|
489
519
|
// Forward text part deltas as chat:delta
|
|
490
520
|
const textDelta = extractTextDelta(envelope);
|
|
491
521
|
if (textDelta) {
|
|
522
|
+
// Bug S3 — drop the delta (with warning) if this session
|
|
523
|
+
// has hit the per-connection cap. The upstream SSE pump
|
|
524
|
+
// continues, but we stop forwarding to WS to avoid
|
|
525
|
+
// unbounded buffering here.
|
|
526
|
+
if (!noteChatDelta(chatSessionId)) return;
|
|
492
527
|
broadcast({
|
|
493
528
|
type: 'chat:delta',
|
|
494
529
|
sessionId: chatSessionId,
|
|
@@ -501,6 +536,9 @@ async function streamOpencodeSession({
|
|
|
501
536
|
onIdle(envelope) {
|
|
502
537
|
if (done) return;
|
|
503
538
|
done = true;
|
|
539
|
+
// Bug S3 — release the per-session delta counter on idle so
|
|
540
|
+
// the next prompt starts with a fresh budget.
|
|
541
|
+
resetChatDeltaCount(chatSessionId);
|
|
504
542
|
// Fetch the final message list and extract the assistant reply.
|
|
505
543
|
void (async () => {
|
|
506
544
|
try {
|
|
@@ -127,13 +127,17 @@ export function createLightragRouter({ projectRoot }) {
|
|
|
127
127
|
const content = readFileSync(logFile, 'utf8');
|
|
128
128
|
const lines = content.split('\n');
|
|
129
129
|
logTail.push(...lines.slice(-30));
|
|
130
|
-
} catch {
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn('swallowed in lightrag log tail:', err.message);
|
|
132
|
+
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
let logSize = 0;
|
|
134
136
|
try {
|
|
135
137
|
if (existsSync(logFile)) logSize = statSync(logFile).size;
|
|
136
|
-
} catch {
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn('swallowed in lightrag log stat:', err.message);
|
|
140
|
+
}
|
|
137
141
|
|
|
138
142
|
res.json({
|
|
139
143
|
running: alive,
|
|
@@ -78,7 +78,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
78
78
|
try {
|
|
79
79
|
const data = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
80
80
|
lastSecretScan = data.attemptedAt || null;
|
|
81
|
-
} catch {
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('swallowed in memory reindex marker read:', err.message);
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
res.json({
|
|
@@ -486,7 +488,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
486
488
|
conflicts.push({ relPath: note.relPath, reason: 'git conflict markers' });
|
|
487
489
|
}
|
|
488
490
|
}
|
|
489
|
-
} catch {
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.warn('swallowed in memory conflict scan:', err.message);
|
|
493
|
+
}
|
|
490
494
|
|
|
491
495
|
res.json({ conflicts });
|
|
492
496
|
}));
|
|
@@ -523,7 +527,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
523
527
|
try {
|
|
524
528
|
const { recordQuery } = await getMemoryLightrag();
|
|
525
529
|
recordQuery(projectRoot, Date.now() - startedAt);
|
|
526
|
-
} catch {
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.warn('swallowed in memory recordQuery:', err.message);
|
|
532
|
+
}
|
|
527
533
|
res.json({ ok: true, q, lexical, semantic });
|
|
528
534
|
}));
|
|
529
535
|
|
|
@@ -605,7 +611,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
605
611
|
const content = readFileSync(logFile, 'utf8');
|
|
606
612
|
const lines = content.split('\n');
|
|
607
613
|
logTail.push(...lines.slice(-30));
|
|
608
|
-
} catch {
|
|
614
|
+
} catch (err) {
|
|
615
|
+
console.warn('swallowed in memory log tail:', err.message);
|
|
616
|
+
}
|
|
609
617
|
}
|
|
610
618
|
|
|
611
619
|
let lastError = null;
|
|
@@ -34,8 +34,8 @@ function logLine(line) {
|
|
|
34
34
|
try {
|
|
35
35
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
36
36
|
appendFileSync(LOG_FILE, line + '\n', 'utf8');
|
|
37
|
-
} catch {
|
|
38
|
-
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn('swallowed in schedule log append:', err.message);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -288,8 +288,8 @@ export const schedulesRunner = {
|
|
|
288
288
|
const projectIds = new Set(reg.projects.map((project) => project.id));
|
|
289
289
|
try {
|
|
290
290
|
if (schedulesStore.list('default').length > 0) projectIds.add('default');
|
|
291
|
-
} catch {
|
|
292
|
-
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.warn('swallowed in schedules due-list:', err.message);
|
|
293
293
|
}
|
|
294
294
|
let skipped = 0;
|
|
295
295
|
for (const projectId of projectIds) {
|