@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.
Files changed (48) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  2. package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
  3. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  5. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-BRhoDOUz.js} +1 -2
  6. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-BRhoDOUz.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-lbH6szyX.js} +2 -3
  8. package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/src/server/headroom.mjs +37 -35
  12. package/bizar-dash/src/server/memory-lightrag.mjs +39 -31
  13. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  14. package/bizar-dash/src/server/providers-store.mjs +73 -1
  15. package/bizar-dash/src/server/routes/chat.mjs +38 -0
  16. package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
  17. package/bizar-dash/src/server/routes/memory.mjs +12 -4
  18. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  19. package/bizar-dash/src/server/server.mjs +39 -37
  20. package/bizar-dash/src/server/watcher.mjs +2 -2
  21. package/bizar-dash/src/web/App.tsx +40 -6
  22. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  23. package/bizar-dash/src/web/components/Topbar.tsx +1 -1
  24. package/bizar-dash/src/web/lib/api.ts +12 -11
  25. package/bizar-dash/src/web/styles/chat.css +2 -0
  26. package/bizar-dash/src/web/styles/main.css +52 -23
  27. package/bizar-dash/src/web/styles/tasks.css +2 -0
  28. package/bizar-dash/src/web/views/Memory.tsx +4 -3
  29. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  30. package/bizar-dash/src/web/views/Overview.tsx +4 -2
  31. package/bizar-dash/src/web/views/Settings.tsx +2 -1
  32. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  33. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  34. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
  35. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  36. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  37. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  38. package/cli/artifact.mjs +11 -4
  39. package/cli/bin.mjs +46 -12
  40. package/cli/doctor.mjs +5 -3
  41. package/cli/memory.mjs +65 -21
  42. package/cli/provision.mjs +8 -2
  43. package/install.sh +1 -2
  44. package/package.json +3 -3
  45. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
  46. package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
  47. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
  48. 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-usWhlPWa.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/mobile-O6ANdD4W.js">
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-eWZ4NlCL.css">
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-DQLFCjwJ.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/mobile-O6ANdD4W.js">
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
- if (process.platform === 'linux') {
121
- const netTcp = readFileSync('/proc/net/tcp', 'utf8');
122
- const portHex = port.toString(16).toUpperCase().padStart(4, '0');
123
- const lines = netTcp.split('\n');
124
- for (const line of lines.slice(1)) {
125
- // Format: sl local_address rem_address st tx_queue rx_queue ... inode
126
- const parts = line.trim().split(/\s+/);
127
- if (parts.length < 10) continue;
128
- const local = parts[1];
129
- const localPortHex = local.split(':')[1];
130
- if (localPortHex?.toUpperCase() === portHex) {
131
- // Try to get PID from the socket inode
132
- const inode = parts[9];
133
- if (inode && inode !== '0') {
134
- // Scan /proc/*/fd/* for socket inodes
135
- const { readdirSync, readlinkSync } = await import('node:fs');
136
- const procDir = '/proc';
137
- const pids = readdirSync(procDir).filter(
138
- (n) => /^\d+$/.test(n) && !isNaN(parseInt(n, 10)),
139
- );
140
- for (const pid of pids) {
141
- try {
142
- const fdDir = join(procDir, pid, 'fd');
143
- const fds = readdirSync(fdDir);
144
- for (const fd of fds) {
145
- try {
146
- const link = readlinkSync(join(fdDir, fd));
147
- if (link.includes(`socket:[${inode}]`)) {
148
- return parseInt(pid, 10);
149
- }
150
- } catch { /* skip */ }
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
- /* marker corrupt ignore */
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
- /* skip */
864
+ } catch (err) {
865
+ console.warn('[lightrag] swallowed in stats.kvStoreRead:', err?.message || err);
858
866
  }
859
867
  }
860
868
  }
861
- } catch {
862
- /* ignore */
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
- /* ignore leave noteCount at 0 */
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
- /* skip */
931
+ } catch (err) {
932
+ console.warn('[lightrag] swallowed in stats.queryLogLine:', err?.message || err);
925
933
  }
926
934
  }
927
- } catch {
928
- /* ignore */
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
- /* ignore */
985
+ } catch (err) {
986
+ console.warn('[lightrag] swallowed in recordQuery.truncate:', err?.message || err);
979
987
  }
980
- } catch {
981
- /* never throw */
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
- /* fall back to manual write */
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
- /* swallow stats are best-effort */
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
- /* skip */
1035
+ } catch (err) {
1036
+ console.warn('[lightrag] swallowed in rebuildGraph.unlinkEntry:', err?.message || err);
1029
1037
  }
1030
1038
  }
1031
1039
  }
1032
- } catch {
1033
- /* ignore */
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 { /* ignore */ }
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 { /* ignore */ }
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 { /* ignore */ }
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 safeReadJSON(OPENCODE_JSON, {});
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 { /* ignore */ }
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 { /* ignore */ }
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 { /* ignore */ }
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 { /* ignore */ }
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 { /* ignore */ }
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
- /* ignore */
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
- /* ignore */
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) {