superlocalmemory 3.6.13 → 3.6.14

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 (124) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +187 -741
  3. package/package.json +12 -5
  4. package/plugin/.claude-plugin/plugin.json +20 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/CLAUDE.md +43 -0
  7. package/plugin/_GENERATED.md +6 -0
  8. package/plugin/agents/slm-memory-advisor.md +43 -0
  9. package/plugin/agents/slm-optimize-advisor.md +38 -0
  10. package/plugin/hooks/hooks.json +14 -0
  11. package/plugin/requirements.txt +1 -0
  12. package/plugin/scripts/ensure-venv.bat +122 -0
  13. package/plugin/scripts/ensure-venv.sh +105 -0
  14. package/plugin/scripts/slm-launch +15 -0
  15. package/plugin/scripts/slm-launch.bat +17 -0
  16. package/plugin/settings.json +16 -0
  17. package/plugin/skills/slm-cache/SKILL.md +140 -0
  18. package/plugin/skills/slm-compress/SKILL.md +143 -0
  19. package/plugin/skills/slm-graph/SKILL.md +300 -0
  20. package/plugin/skills/slm-recall/SKILL.md +196 -0
  21. package/plugin/skills/slm-remember/SKILL.md +182 -0
  22. package/plugin/skills/slm-session/SKILL.md +207 -0
  23. package/plugin/skills/slm-status/SKILL.md +149 -0
  24. package/plugin-src/.mcp.json +12 -0
  25. package/plugin-src/agents/slm-memory-advisor.md +43 -0
  26. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  27. package/plugin-src/commands/slm-optimize.md +22 -0
  28. package/plugin-src/commands/slm-recall.md +16 -0
  29. package/plugin-src/commands/slm-remember.md +16 -0
  30. package/plugin-src/commands/slm-status.md +15 -0
  31. package/plugin-src/hooks/.gitkeep +0 -0
  32. package/plugin-src/hooks/hooks.json +14 -0
  33. package/plugin-src/manifest.json +25 -0
  34. package/plugin-src/requirements.txt +1 -0
  35. package/plugin-src/rules/AGENTS.md +90 -0
  36. package/plugin-src/rules/CLAUDE.md.fragment +43 -0
  37. package/plugin-src/scripts/ensure-venv.bat +122 -0
  38. package/plugin-src/scripts/ensure-venv.sh +105 -0
  39. package/plugin-src/scripts/slm-launch +15 -0
  40. package/plugin-src/scripts/slm-launch.bat +17 -0
  41. package/plugin-src/settings.json +16 -0
  42. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  43. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  45. package/plugin-src/skills/slm-recall/SKILL.md +196 -0
  46. package/plugin-src/skills/slm-remember/SKILL.md +182 -0
  47. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  48. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  49. package/pyproject.toml +6 -2
  50. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  51. package/scripts/_savings_math.py +270 -0
  52. package/scripts/build-plugin.js +742 -0
  53. package/scripts/dogfood_savings.py +490 -0
  54. package/scripts/install-skills.ps1 +4 -334
  55. package/scripts/install-skills.sh +4 -435
  56. package/scripts/postinstall-interactive.js +0 -27
  57. package/scripts/postinstall.js +21 -2
  58. package/src/superlocalmemory/__init__.py +1 -1
  59. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  60. package/src/superlocalmemory/cli/commands.py +348 -39
  61. package/src/superlocalmemory/cli/main.py +47 -4
  62. package/src/superlocalmemory/cli/setup_wizard.py +20 -6
  63. package/src/superlocalmemory/core/config.py +79 -9
  64. package/src/superlocalmemory/core/embeddings.py +10 -5
  65. package/src/superlocalmemory/core/engine.py +2 -2
  66. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  67. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  68. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  69. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  70. package/src/superlocalmemory/mcp/server.py +75 -4
  71. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  72. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  73. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  74. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  75. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  76. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  77. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  78. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  79. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  80. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  81. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  82. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  83. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  84. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  85. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  86. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  87. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  88. package/src/superlocalmemory/server/unified_daemon.py +24 -6
  89. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  90. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  91. package/src/superlocalmemory/ui/index.html +2 -2
  92. package/src/superlocalmemory/ui/js/core.js +98 -0
  93. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  94. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  95. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  96. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  97. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  98. package/src/superlocalmemory.egg-info/PKG-INFO +189 -742
  99. package/src/superlocalmemory.egg-info/SOURCES.txt +6 -9
  100. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  101. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  102. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  103. package/ide/skills/slm-recall/SKILL.md +0 -326
  104. package/ide/skills/slm-remember/SKILL.md +0 -194
  105. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  106. package/ide/skills/slm-status/SKILL.md +0 -363
  107. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  108. package/skills/slm-build-graph/SKILL.md +0 -423
  109. package/skills/slm-list-recent/SKILL.md +0 -348
  110. package/skills/slm-optimize/README.md +0 -55
  111. package/skills/slm-optimize/SKILL.md +0 -139
  112. package/skills/slm-recall/SKILL.md +0 -343
  113. package/skills/slm-remember/SKILL.md +0 -194
  114. package/skills/slm-show-patterns/SKILL.md +0 -224
  115. package/skills/slm-status/SKILL.md +0 -363
  116. package/skills/slm-switch-profile/SKILL.md +0 -442
  117. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  118. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  119. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  120. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  121. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  122. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  123. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  124. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
@@ -947,6 +947,36 @@ class CacheDB:
947
947
  except sqlite3.Error as exc:
948
948
  logger.warning("CacheDB.ccr_update_compressed failed: %s", exc)
949
949
 
950
+ def ccr_delete(self, ccr_id: str) -> None:
951
+ """Delete a CCR row by ccr_id. Idempotent — warns on sqlite error, never raises.
952
+
953
+ WP-10 D6: defensive infra + sweep parity. Deleting a non-existent row is a no-op.
954
+ """
955
+ try:
956
+ self._db.execute(
957
+ "DELETE FROM llmcache_ccr_originals WHERE ccr_id = ?",
958
+ (ccr_id,),
959
+ )
960
+ except sqlite3.Error as exc:
961
+ logger.warning("CacheDB.ccr_delete failed (non-fatal): %s", exc)
962
+
963
+ def ccr_count(self) -> int:
964
+ """Return UNFILTERED count of rows in llmcache_ccr_originals.
965
+
966
+ WP-10 CRIT-2: Do NOT reuse TTL-filtered count at :646. A fresh no-expiry row
967
+ has ttl_expires=None, so the TTL filter returns 0 and D6 orphan tests would
968
+ falsely pass. This unfiltered count is test infrastructure only.
969
+ """
970
+ try:
971
+ rows = self._db.execute(
972
+ "SELECT COUNT(*) AS n FROM llmcache_ccr_originals",
973
+ (),
974
+ )
975
+ return int(dict(rows[0])["n"]) if rows else 0
976
+ except sqlite3.Error as exc:
977
+ logger.warning("CacheDB.ccr_count failed: %s", exc)
978
+ return 0
979
+
950
980
  # ---- v2 additions ----
951
981
 
952
982
  def get_all_vectors(self, tenant_id: str) -> list[tuple[str, bytes, str]]:
@@ -23,6 +23,8 @@ from __future__ import annotations
23
23
  import re
24
24
  from typing import Any
25
25
 
26
+ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
27
+
26
28
 
27
29
  # ---------------------------------------------------------------------------
28
30
  # F-2: Per-fact content clamp
@@ -161,7 +163,7 @@ def apply_source_content_discipline(
161
163
  def serialize_recall_response(
162
164
  response: Any,
163
165
  *,
164
- limit: int = 10,
166
+ limit: int = CANONICAL_RECALL_LIMIT,
165
167
  memory_map: dict[str, str] | None = None,
166
168
  per_fact_max: int = 2400,
167
169
  total_max: int = 12000,
@@ -50,6 +50,8 @@ from fastapi.middleware.cors import CORSMiddleware
50
50
  from fastapi.middleware.gzip import GZipMiddleware
51
51
  from pydantic import BaseModel
52
52
 
53
+ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
54
+
53
55
  logger = logging.getLogger("superlocalmemory.unified_daemon")
54
56
 
55
57
  _DEFAULT_PORT = 8765
@@ -237,16 +239,32 @@ class ObserveBuffer:
237
239
  try:
238
240
  from superlocalmemory.hooks.auto_capture import AutoCapture
239
241
  auto = AutoCapture(engine=self._engine)
242
+ captured_count = 0
243
+ failed_count = 0
240
244
  for content in batch:
241
245
  try:
242
246
  decision = auto.evaluate(content)
243
247
  if decision.capture:
244
248
  auto.capture(content, category=decision.category)
245
- except Exception:
246
- pass
247
- logger.info("Observe debounce: processed %d observations", len(batch))
248
- except Exception:
249
- pass
249
+ # Stage-9: count only what was actually WRITTEN to memory.
250
+ # The prior 'processed N' counted skipped (capture=False)
251
+ # items as successes a false-positive write count.
252
+ captured_count += 1
253
+ except Exception as exc:
254
+ failed_count += 1
255
+ logger.warning(
256
+ "ObserveBuffer: auto.capture failed for content %.40r: %s",
257
+ content,
258
+ exc,
259
+ )
260
+ logger.info(
261
+ "Observe debounce: evaluated=%d captured=%d failed=%d",
262
+ len(batch),
263
+ captured_count,
264
+ failed_count,
265
+ )
266
+ except Exception as exc:
267
+ logger.error("ObserveBuffer: flush batch failed: %s", exc)
250
268
 
251
269
  def flush_sync(self) -> None:
252
270
  """Force flush for shutdown."""
@@ -1620,7 +1638,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
1620
1638
  @application.get("/recall")
1621
1639
  async def recall(
1622
1640
  request: Request,
1623
- q: str = "", query: str = "", limit: int = 20,
1641
+ q: str = "", query: str = "", limit: int = CANONICAL_RECALL_LIMIT,
1624
1642
  session_id: str = "",
1625
1643
  fast: bool = False,
1626
1644
  full: bool = False,
@@ -202,6 +202,24 @@
202
202
  margin-bottom: 12px;
203
203
  }
204
204
 
205
+ /* Pane error state — WP-12 */
206
+ .pane-error {
207
+ text-align: center;
208
+ padding: 40px;
209
+ color: var(--bs-danger, #dc3545);
210
+ }
211
+
212
+ .pane-error i {
213
+ font-size: 2rem;
214
+ display: block;
215
+ margin-bottom: 8px;
216
+ opacity: 0.7;
217
+ }
218
+
219
+ .pane-error p {
220
+ margin-bottom: 12px;
221
+ }
222
+
205
223
  .tooltip-custom {
206
224
  position: absolute;
207
225
  padding: 10px;
@@ -1586,3 +1586,8 @@ body.ng-privacy-blur::before {
1586
1586
  max-width: 1600px;
1587
1587
  }
1588
1588
  }
1589
+
1590
+ /* WP-12 — pane-error dark-mode override */
1591
+ .ng-dark .pane-error {
1592
+ color: var(--ng-status-error);
1593
+ }
@@ -1242,7 +1242,7 @@
1242
1242
  </div>
1243
1243
  <div class="card-body">
1244
1244
  <div class="row g-3">
1245
- <div class="col-md-6">
1245
+ <div class="col-md-6" id="optimize-config-card">
1246
1246
  <h6>Controls</h6>
1247
1247
  <div class="form-check form-switch mb-2">
1248
1248
  <input class="form-check-input" type="checkbox" id="opt-enabled">
@@ -1282,7 +1282,7 @@
1282
1282
  <label class="form-check-label" for="opt-compress-prose">Prose Compression</label>
1283
1283
  </div>
1284
1284
  </div>
1285
- <div class="col-md-6">
1285
+ <div class="col-md-6" id="optimize-savings-card">
1286
1286
  <h6>Savings</h6>
1287
1287
  <table class="table table-sm">
1288
1288
  <tr><td>Tokens Saved</td><td id="opt-tokens-saved">-</td></tr>
@@ -199,6 +199,104 @@ function showEmpty(containerId, icon, message) {
199
199
  el.appendChild(wrapper);
200
200
  }
201
201
 
202
+ // ============================================================================
203
+ // Pane error state — WP-12
204
+ // showPaneError / clearPaneError / paneErrorMessage
205
+ // ============================================================================
206
+
207
+ /**
208
+ * Map an HTTP status (or 0 for network failure) to a user-readable message.
209
+ * @param {number} status 0 = network/abort, ≥400 = HTTP error
210
+ * @returns {string}
211
+ */
212
+ function paneErrorMessage(status) {
213
+ if (!status || status === 0) {
214
+ return 'Service unavailable — check network connection';
215
+ }
216
+ if (status >= 500) {
217
+ return 'Server error ' + status + ' — daemon may be down';
218
+ }
219
+ return 'Request failed ' + status;
220
+ }
221
+
222
+ /**
223
+ * Render an inline error banner inside a pane container.
224
+ *
225
+ * slotMode=false (container panes — math-health, ide-status):
226
+ * Clears the container and appends the error div directly.
227
+ *
228
+ * slotMode=true (field-scatter panes — dashboard, trust, optimize):
229
+ * Inserts/replaces a #<containerId>-error-slot div at the top of the
230
+ * container, leaving existing field values visible.
231
+ *
232
+ * @param {string} containerId - getElementById target
233
+ * @param {string} message - user-facing message (set via textContent — XSS-safe)
234
+ * @param {Function|null} onRetry - callback for Retry button; null = no button
235
+ * @param {boolean} slotMode - true for field-scatter panes
236
+ */
237
+ function showPaneError(containerId, message, onRetry, slotMode) {
238
+ var el = document.getElementById(containerId);
239
+ if (!el) return;
240
+
241
+ // Build the error div
242
+ var errDiv = document.createElement('div');
243
+ errDiv.className = 'pane-error';
244
+ errDiv.setAttribute('role', 'alert');
245
+
246
+ var icon = document.createElement('i');
247
+ icon.className = 'bi bi-exclamation-triangle';
248
+ errDiv.appendChild(icon);
249
+
250
+ var p = document.createElement('p');
251
+ p.textContent = message; // textContent — no XSS risk
252
+ errDiv.appendChild(p);
253
+
254
+ if (typeof onRetry === 'function') {
255
+ var btn = document.createElement('button');
256
+ btn.className = 'btn btn-sm btn-outline-danger pane-error-retry';
257
+ btn.textContent = 'Retry';
258
+ // Capture onRetry lexically to avoid closure issues in IIFEs (CRIT-1)
259
+ (function(cb) {
260
+ btn.addEventListener('click', function() { cb(); });
261
+ }(onRetry));
262
+ errDiv.appendChild(btn);
263
+ }
264
+
265
+ if (slotMode) {
266
+ // Insert/replace error slot at top of container
267
+ var slotId = containerId + '-error-slot';
268
+ errDiv.id = slotId;
269
+ var existing = document.getElementById(slotId);
270
+ if (existing) {
271
+ existing.parentNode.replaceChild(errDiv, existing);
272
+ } else {
273
+ el.insertBefore(errDiv, el.firstChild);
274
+ }
275
+ } else {
276
+ // Replace container contents
277
+ el.textContent = '';
278
+ el.appendChild(errDiv);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Remove the error state previously set by showPaneError.
284
+ *
285
+ * @param {string} containerId - getElementById target
286
+ * @param {boolean} slotMode - must match the slotMode used in showPaneError
287
+ */
288
+ function clearPaneError(containerId, slotMode) {
289
+ if (slotMode) {
290
+ var slot = document.getElementById(containerId + '-error-slot');
291
+ if (slot && slot.parentNode) slot.parentNode.removeChild(slot);
292
+ } else {
293
+ var el = document.getElementById(containerId);
294
+ if (!el) return;
295
+ var err = el.querySelector('.pane-error');
296
+ if (err) el.removeChild(err);
297
+ }
298
+ }
299
+
202
300
  // ============================================================================
203
301
  // Safe HTML builder — tagged template for escaped interpolation
204
302
  // ============================================================================
@@ -17,11 +17,17 @@ window.addEventListener('hashchange', function() {
17
17
  window.addEventListener('focus', function() { loadDashboard(); });
18
18
 
19
19
  async function loadDashboard() {
20
+ var PANE_ID = 'dashboard-pane';
20
21
  try {
21
22
  var response = await fetch('/api/v3/dashboard');
22
- if (!response.ok) return;
23
+ if (!response.ok) {
24
+ showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
25
+ return;
26
+ }
23
27
  var data = await response.json();
24
28
 
29
+ clearPaneError(PANE_ID, true);
30
+
25
31
  document.getElementById('dashboard-mode').textContent = 'Mode ' + data.mode.toUpperCase();
26
32
  document.getElementById('dashboard-mode-desc').textContent = data.mode_name + (data.provider !== 'none' ? ' — ' + data.provider : '');
27
33
  document.getElementById('dashboard-memory-count').textContent = data.fact_count || data.memory_count || '0';
@@ -44,6 +50,7 @@ async function loadDashboard() {
44
50
  btn.classList.toggle('active', btn.dataset.mode === data.mode);
45
51
  });
46
52
  } catch (e) {
53
+ showPaneError(PANE_ID, paneErrorMessage(0), loadDashboard, true);
47
54
  console.log('Dashboard load error:', e);
48
55
  }
49
56
  }
@@ -2,14 +2,26 @@
2
2
  // Displays detected IDEs and allows connecting them to SLM.
3
3
 
4
4
  async function loadIDEStatus() {
5
+ var CID = 'ide-list-body';
5
6
  try {
6
7
  var response = await fetch('/api/v3/ide/status');
7
- if (!response.ok) return;
8
+ if (!response.ok) {
9
+ showPaneError(CID, paneErrorMessage(response.status), loadIDEStatus, false);
10
+ return;
11
+ }
8
12
  var data = await response.json();
9
13
 
10
- var tbody = document.getElementById('ide-list-body');
14
+ var ides = data.ides || [];
15
+
16
+ // AC5: empty array → empty state
17
+ if (ides.length === 0) {
18
+ showEmpty(CID, 'laptop', 'No IDEs detected');
19
+ return;
20
+ }
21
+
22
+ var tbody = document.getElementById(CID);
11
23
  tbody.textContent = '';
12
- (data.ides || []).forEach(function(ide) {
24
+ ides.forEach(function(ide) {
13
25
  var tr = document.createElement('tr');
14
26
 
15
27
  // IDE name cell
@@ -52,6 +64,7 @@ async function loadIDEStatus() {
52
64
  tbody.appendChild(tr);
53
65
  });
54
66
  } catch (e) {
67
+ showPaneError('ide-list-body', paneErrorMessage(0), loadIDEStatus, false);
55
68
  console.log('IDE status error:', e);
56
69
  }
57
70
  }
@@ -2,15 +2,26 @@
2
2
  // Displays status of Fisher-Rao, sheaf cohomology, and Langevin dynamics layers.
3
3
 
4
4
  async function loadMathHealth() {
5
+ var CID = 'math-health-cards';
5
6
  try {
6
7
  var response = await fetch('/api/v3/math/health');
7
- if (!response.ok) return;
8
+ if (!response.ok) {
9
+ showPaneError(CID, paneErrorMessage(response.status), loadMathHealth, false);
10
+ return;
11
+ }
8
12
  var data = await response.json();
9
13
 
10
- var container = document.getElementById('math-health-cards');
14
+ var layers = data.health || {};
15
+
16
+ // AC5: empty object → empty state
17
+ if (!layers || Object.keys(layers).length === 0) {
18
+ showEmpty(CID, 'calculator', 'No math health data available');
19
+ return;
20
+ }
21
+
22
+ var container = document.getElementById(CID);
11
23
  container.textContent = '';
12
24
 
13
- var layers = data.health || {};
14
25
  var colors = { fisher: 'primary', sheaf: 'success', langevin: 'info' };
15
26
  var icons = { fisher: 'bi-graph-up', sheaf: 'bi-diagram-3', langevin: 'bi-activity' };
16
27
 
@@ -91,6 +102,7 @@ async function loadMathHealth() {
91
102
  container.appendChild(col);
92
103
  });
93
104
  } catch (e) {
105
+ showPaneError('math-health-cards', paneErrorMessage(0), loadMathHealth, false);
94
106
  console.log('Math health error:', e);
95
107
  }
96
108
  }
@@ -17,11 +17,17 @@
17
17
  _pollTimer = setInterval(_loadSavings, 10000);
18
18
  }
19
19
 
20
+ var CFG_CARD = 'optimize-config-card';
21
+
20
22
  async function _loadOptimizeConfig() {
21
23
  try {
22
24
  var resp = await fetch('/api/optimize/config');
23
- if (!resp.ok) return;
25
+ if (!resp.ok) {
26
+ showPaneError(CFG_CARD, paneErrorMessage(resp.status), _loadOptimizeConfig, true);
27
+ return;
28
+ }
24
29
  var cfg = await resp.json();
30
+ clearPaneError(CFG_CARD, true);
25
31
  _setToggle('opt-enabled', cfg.enabled);
26
32
  _setToggle('opt-proxy-enabled', cfg.proxy_enabled);
27
33
  _setToggle('opt-cache-enabled', cfg.cache_enabled);
@@ -32,15 +38,23 @@
32
38
  var verEl = document.getElementById('opt-config-version');
33
39
  if (verEl) verEl.textContent = cfg.config_version || '-';
34
40
  } catch (e) {
41
+ showPaneError(CFG_CARD, paneErrorMessage(0), _loadOptimizeConfig, true);
35
42
  console.log('Optimize config load error:', e);
36
43
  }
37
44
  }
38
45
 
46
+ var SAV_CARD = 'optimize-savings-card';
47
+
39
48
  async function _loadSavings() {
40
49
  try {
41
50
  var resp = await fetch('/api/optimize/savings');
42
- if (!resp.ok) return;
51
+ if (!resp.ok) {
52
+ // D-3: no Retry on polling loader \u2014 auto-heals on next poll
53
+ showPaneError(SAV_CARD, paneErrorMessage(resp.status), null, true);
54
+ return;
55
+ }
43
56
  var data = await resp.json();
57
+ clearPaneError(SAV_CARD, true);
44
58
  var tokensSaved = (data.tokens_saved_input || 0) + (data.tokens_saved_output || 0) + (data.tokens_saved_compress || 0);
45
59
  _setText('opt-tokens-saved', tokensSaved.toLocaleString());
46
60
  var costSaved = data.cost_saved || {};
@@ -57,6 +71,8 @@
57
71
  _setText('opt-stale-warning', 'Pricing data may be outdated');
58
72
  }
59
73
  } catch (e) {
74
+ // D-3: no Retry on polling loader
75
+ showPaneError(SAV_CARD, paneErrorMessage(0), null, true);
60
76
  console.log('Savings load error:', e);
61
77
  }
62
78
  }
@@ -127,12 +127,20 @@
127
127
  if (next) next.disabled = STATE.page >= maxPage;
128
128
  }
129
129
 
130
+ var TRUST_PANE_ID = 'operations-pane';
131
+
130
132
  async function loadTrustDashboard() {
131
133
  try {
132
134
  var resp = await fetch('/api/v3/trust/dashboard');
133
- if (!resp.ok) return;
135
+ if (!resp.ok) {
136
+ // CRIT-1: pass lexically-scoped loadTrustDashboard (inside IIFE)
137
+ showPaneError(TRUST_PANE_ID, paneErrorMessage(resp.status), loadTrustDashboard, true);
138
+ return;
139
+ }
134
140
  var data = await resp.json();
135
141
 
142
+ clearPaneError(TRUST_PANE_ID, true);
143
+
136
144
  var agents = data.agents || [];
137
145
  STATE.agents = agents;
138
146
  STATE.page = 0;
@@ -154,6 +162,7 @@
154
162
 
155
163
  _renderPage();
156
164
  } catch (e) {
165
+ showPaneError(TRUST_PANE_ID, paneErrorMessage(0), loadTrustDashboard, true);
157
166
  if (window.console && window.console.debug) {
158
167
  window.console.debug('Trust dashboard error:', e);
159
168
  }