prism-mcp-server 4.3.0 → 4.6.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.
@@ -657,6 +657,8 @@ export function renderDashboardHTML(version) {
657
657
  <div class="settings-tabs">
658
658
  <button class="s-tab active" id="stab-settings" onclick="switchSettingsTab('settings')">⚙️ Settings</button>
659
659
  <button class="s-tab" id="stab-skills" onclick="switchSettingsTab('skills')">📜 Skills</button>
660
+ <button class="s-tab" id="stab-providers" onclick="switchSettingsTab('providers')">🤖 AI Providers</button>
661
+ <button class="s-tab" id="stab-observability" onclick="switchSettingsTab('observability')">🔭 Observability</button>
660
662
  </div>
661
663
 
662
664
  <!-- Settings panel (existing content) -->
@@ -817,6 +819,225 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
817
819
  </div>
818
820
  </div><!-- /spanel-skills -->
819
821
 
822
+ <!-- AI Providers panel (v4.4) -->
823
+ <div class="s-tab-panel" id="spanel-providers">
824
+
825
+ <div class="setting-section">Text Provider <span class="boot-badge">Restart Required</span></div>
826
+
827
+ <!-- ── Text Provider ──────────────────────────────── -->
828
+ <div class="setting-row">
829
+ <div>
830
+ <div class="setting-label">Text Provider</div>
831
+ <div class="setting-desc">LLM used for compaction, briefing, security scan &amp; fact merging</div>
832
+ </div>
833
+ <select id="select-text-provider"
834
+ style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;"
835
+ onchange="onTextProviderChange(this.value)">
836
+ <option value="gemini">🔵 Gemini (Google)</option>
837
+ <option value="openai">🟢 OpenAI / Ollama</option>
838
+ <option value="anthropic">🟣 Anthropic (Claude)</option>
839
+ </select>
840
+ </div>
841
+
842
+ <!-- Gemini text fields -->
843
+ <div id="provider-fields-gemini">
844
+ <div class="setting-row">
845
+ <div>
846
+ <div class="setting-label">Google API Key</div>
847
+ <div class="setting-desc">GOOGLE_API_KEY — required for Gemini text &amp; embeddings</div>
848
+ </div>
849
+ <input type="password" id="input-google-api-key"
850
+ placeholder="AIza…"
851
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
852
+ onchange="saveBootSetting('GOOGLE_API_KEY', this.value)"
853
+ oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('GOOGLE_API_KEY',this.value),800)" />
854
+ </div>
855
+ </div>
856
+
857
+ <!-- OpenAI / Ollama text fields -->
858
+ <div id="provider-fields-openai" style="display:none">
859
+ <div class="setting-row">
860
+ <div>
861
+ <div class="setting-label">API Key</div>
862
+ <div class="setting-desc">Leave blank for Ollama / LM Studio (local endpoints)</div>
863
+ </div>
864
+ <input type="password" id="input-openai-api-key"
865
+ placeholder="sk-… (blank for Ollama)"
866
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
867
+ onchange="saveBootSetting('openai_api_key', this.value)"
868
+ oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('openai_api_key',this.value),800)" />
869
+ </div>
870
+ <div class="setting-row">
871
+ <div>
872
+ <div class="setting-label">Base URL</div>
873
+ <div class="setting-desc">Ollama: http://localhost:11434/v1 · LM Studio: http://localhost:1234/v1</div>
874
+ </div>
875
+ <input type="text" id="input-openai-base-url"
876
+ placeholder="https://api.openai.com/v1"
877
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;"
878
+ onchange="saveBootSetting('openai_base_url', this.value)"
879
+ oninput="clearTimeout(this._pu); this._pu=setTimeout(()=>saveBootSetting('openai_base_url',this.value),800)" />
880
+ </div>
881
+ <div class="setting-row">
882
+ <div>
883
+ <div class="setting-label">Chat Model</div>
884
+ <div class="setting-desc">Used for compaction, briefing, security scan</div>
885
+ </div>
886
+ <input type="text" id="input-openai-model"
887
+ placeholder="gpt-4o-mini"
888
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 160px;"
889
+ onchange="saveBootSetting('openai_model', this.value)"
890
+ oninput="clearTimeout(this._pm); this._pm=setTimeout(()=>saveBootSetting('openai_model',this.value),800)" />
891
+ </div>
892
+ </div>
893
+
894
+ <!-- Anthropic / Claude text fields -->
895
+ <div id="provider-fields-anthropic" style="display:none">
896
+ <div class="setting-row">
897
+ <div>
898
+ <div class="setting-label">Anthropic API Key</div>
899
+ <div class="setting-desc">Required. Get yours at console.anthropic.com</div>
900
+ </div>
901
+ <input type="password" id="input-anthropic-api-key"
902
+ placeholder="sk-ant-…"
903
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 200px;"
904
+ onchange="saveBootSetting('anthropic_api_key', this.value)"
905
+ oninput="clearTimeout(this._pa); this._pa=setTimeout(()=>saveBootSetting('anthropic_api_key',this.value),800)" />
906
+ </div>
907
+ <div class="setting-row">
908
+ <div>
909
+ <div class="setting-label">Claude Model</div>
910
+ <div class="setting-desc">claude-3-5-sonnet for quality · claude-3-haiku for speed &amp; cost</div>
911
+ </div>
912
+ <input type="text" id="input-anthropic-model"
913
+ placeholder="claude-3-5-sonnet-20241022"
914
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;"
915
+ onchange="saveBootSetting('anthropic_model', this.value)"
916
+ oninput="clearTimeout(this._pam); this._pam=setTimeout(()=>saveBootSetting('anthropic_model',this.value),800)" />
917
+ </div>
918
+ </div>
919
+
920
+ <!-- ── Embedding Provider (always visible) ─────────── -->
921
+ <div class="setting-section" style="margin-top:1.2rem">Embedding Provider <span class="boot-badge">Restart Required</span></div>
922
+
923
+ <div class="setting-row">
924
+ <div>
925
+ <div class="setting-label">Embedding Provider</div>
926
+ <div class="setting-desc">Source for vector embeddings used by semantic memory search</div>
927
+ </div>
928
+ <select id="select-embedding-provider"
929
+ style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;"
930
+ onchange="onEmbeddingProviderChange(this.value)">
931
+ <option value="auto">🔄 Auto (same as Text Provider)</option>
932
+ <option value="gemini">🔵 Gemini</option>
933
+ <option value="openai">🟢 OpenAI / Ollama</option>
934
+ </select>
935
+ </div>
936
+
937
+ <!-- Anthropic + auto warning: shown when text=anthropic AND embed=auto -->
938
+ <div id="anthropic-embed-warning" style="display:none;margin-top:0.5rem;padding:0.5rem 0.75rem;background:rgba(251,146,60,0.1);border:1px solid rgba(251,146,60,0.3);border-radius:6px;font-size:0.78rem;color:#fb923c;line-height:1.5">
939
+ ⚠️ <strong>Anthropic has no native embedding API.</strong>
940
+ Auto mode will route embeddings to <strong>Gemini</strong>.
941
+ Set Embedding Provider to <strong>OpenAI / Ollama</strong> to use a local model (e.g. <code>nomic-embed-text</code>).
942
+ </div>
943
+
944
+ <!-- OpenAI embedding model field (shown when embedding_provider = openai) -->
945
+ <div id="embed-fields-openai" style="display:none">
946
+ <div class="setting-row">
947
+ <div>
948
+ <div class="setting-label">Embedding Model</div>
949
+ <div class="setting-desc">Must output 768 dims. Ollama: nomic-embed-text · OpenAI: text-embedding-3-small</div>
950
+ </div>
951
+ <input type="text" id="input-openai-embedding-model"
952
+ placeholder="text-embedding-3-small"
953
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 210px;"
954
+ onchange="saveBootSetting('openai_embedding_model', this.value)"
955
+ oninput="clearTimeout(this._pe); this._pe=setTimeout(()=>saveBootSetting('openai_embedding_model',this.value),800)" />
956
+ </div>
957
+ </div>
958
+
959
+ <div style="margin-top:1rem;padding:0.6rem 0.8rem;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);border-radius:6px;font-size:0.78rem;color:var(--text-secondary);line-height:1.5">
960
+ 💡 <strong>Cost-optimized setup:</strong> Text Provider → <code>Anthropic</code>, Embedding Provider → <code>OpenAI / Ollama</code>.<br>
961
+ Use Claude 3.5 Sonnet for reasoning &amp; <code>nomic-embed-text</code> (free, local) for embeddings.
962
+ </div>
963
+
964
+ <span class="setting-saved" id="savedToastProviders">Saved ✓</span>
965
+ </div><!-- /spanel-providers -->
966
+
967
+ <!-- ─── Observability panel (v4.6.0 — OTel) ───────────────────────── -->
968
+ <div class="s-tab-panel" id="spanel-observability">
969
+
970
+ <div class="setting-section">OpenTelemetry (OTel)</div>
971
+
972
+ <div class="setting-row" style="align-items: flex-start; margin-bottom: 1rem;">
973
+ <div class="setting-desc" style="margin: 0;">
974
+ Export distributed traces to
975
+ <a href="https://www.jaegertracing.io" target="_blank" rel="noopener" style="color: var(--accent);">Jaeger</a>,
976
+ <a href="https://grafana.com/oss/tempo/" target="_blank" rel="noopener" style="color: var(--accent);">Grafana Tempo</a>, or
977
+ <a href="https://zipkin.io" target="_blank" rel="noopener" style="color: var(--accent);">Zipkin</a>.
978
+ Provides a full latency waterfall for every MCP tool call, LLM provider hop, and background worker task.
979
+ <br><br>
980
+ <code style="font-size: 0.8rem; background: var(--bg-hover); padding: 2px 6px; border-radius: 4px;">
981
+ docker run -d -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one
982
+ </code>
983
+ &nbsp;→ open <a href="http://localhost:16686" target="_blank" rel="noopener" style="color: var(--accent);">localhost:16686</a>
984
+ </div>
985
+ </div>
986
+
987
+ <!-- Enable toggle -->
988
+ <div class="setting-row">
989
+ <div>
990
+ <div class="setting-label">Enable OpenTelemetry</div>
991
+ <div class="setting-desc">Activates the W3C tracing pipeline. <strong>Requires server restart.</strong></div>
992
+ </div>
993
+ <label class="toggle-switch">
994
+ <input type="checkbox" id="input-otel-enabled"
995
+ onchange="saveBootSetting('otel_enabled', this.checked ? 'true' : 'false')">
996
+ <span class="slider"></span>
997
+ </label>
998
+ </div>
999
+
1000
+ <!-- OTLP endpoint -->
1001
+ <div class="setting-row">
1002
+ <div style="flex: 0 0 auto; min-width: 160px;">
1003
+ <div class="setting-label">OTLP HTTP Endpoint</div>
1004
+ <div class="setting-desc">Where spans are exported.</div>
1005
+ </div>
1006
+ <input type="text" id="input-otel-endpoint"
1007
+ class="setting-input"
1008
+ placeholder="http://localhost:4318/v1/traces"
1009
+ style="flex: 1;"
1010
+ onchange="saveBootSetting('otel_endpoint', this.value)"
1011
+ oninput="clearTimeout(this._pt); this._pt=setTimeout(()=>saveBootSetting('otel_endpoint',this.value),800)" />
1012
+ </div>
1013
+
1014
+ <!-- Service name -->
1015
+ <div class="setting-row">
1016
+ <div style="flex: 0 0 auto; min-width: 160px;">
1017
+ <div class="setting-label">Service Name</div>
1018
+ <div class="setting-desc">Label shown in the trace UI.</div>
1019
+ </div>
1020
+ <input type="text" id="input-otel-service"
1021
+ class="setting-input"
1022
+ placeholder="prism-mcp-server"
1023
+ style="flex: 1;"
1024
+ onchange="saveBootSetting('otel_service_name', this.value)"
1025
+ oninput="clearTimeout(this._ps); this._ps=setTimeout(()=>saveBootSetting('otel_service_name',this.value),800)" />
1026
+ </div>
1027
+
1028
+ <!-- Expected trace waterfall diagram -->
1029
+ <div class="setting-row" style="flex-direction: column; align-items: flex-start; margin-top: 0.5rem;">
1030
+ <div class="setting-label" style="margin-bottom: 0.5rem;">Expected Trace Waterfall</div>
1031
+ <pre style="font-size: 0.78rem; background: var(--bg-hover); padding: 0.8rem 1rem; border-radius: 6px; color: var(--text-secondary); line-height: 1.6; width: 100%; box-sizing: border-box; overflow-x: auto;">mcp.call_tool [e.g. session_save_image, ~50 ms]
1032
+ └─ worker.vlm_caption [~2–5 s, outlives parent ✓]
1033
+ └─ llm.generate_image_description [~1–4 s]
1034
+ └─ llm.generate_embedding [~200 ms]</pre>
1035
+ </div>
1036
+
1037
+ <span class="setting-saved" id="savedToastOtel">Saved ✓</span>
1038
+ </div><!-- /spanel-observability -->
1039
+
1040
+
820
1041
  </div>
821
1042
  </div>
822
1043
  </div>
@@ -1246,15 +1467,20 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1246
1467
  var _skillsCache = {}; // role → content cache
1247
1468
 
1248
1469
  function switchSettingsTab(tab) {
1249
- ['settings','skills'].forEach(function(t) {
1470
+ ['settings','skills','providers','observability'].forEach(function(t) {
1250
1471
  document.getElementById('stab-' + t).classList.toggle('active', t === tab);
1251
1472
  document.getElementById('spanel-' + t).classList.toggle('active', t === tab);
1252
1473
  });
1253
1474
  if (tab === 'skills') {
1254
- // Load skill for whichever role is currently selected
1255
1475
  var role = document.getElementById('skillRoleSelect').value;
1256
1476
  loadSkillForRole(role);
1257
1477
  }
1478
+ if (tab === 'providers') {
1479
+ loadAiProviderSettings();
1480
+ }
1481
+ if (tab === 'observability') {
1482
+ loadOtelSettings();
1483
+ }
1258
1484
  }
1259
1485
 
1260
1486
  async function loadSkillForRole(role) {
@@ -1310,6 +1536,84 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1310
1536
  input.value = ''; // reset so same file can be re-uploaded
1311
1537
  }
1312
1538
 
1539
+ // ─── AI Providers Settings (v4.4) ────────────────────────────────────
1540
+ // text_provider → governs generateText() (gemini | openai | anthropic)
1541
+ // embedding_provider → governs generateEmbedding() (auto | gemini | openai)
1542
+
1543
+ // Called when the TEXT provider dropdown changes.
1544
+ function onTextProviderChange(value) {
1545
+ document.getElementById('provider-fields-gemini').style.display = value === 'gemini' ? '' : 'none';
1546
+ document.getElementById('provider-fields-openai').style.display = value === 'openai' ? '' : 'none';
1547
+ document.getElementById('provider-fields-anthropic').style.display = value === 'anthropic' ? '' : 'none';
1548
+ // Refresh the Anthropic warning — its visibility depends on both dropdowns
1549
+ refreshAnthropicWarning(value, document.getElementById('select-embedding-provider').value);
1550
+ saveBootSetting('text_provider', value);
1551
+ }
1552
+
1553
+ // Called when the EMBEDDING provider dropdown changes.
1554
+ function onEmbeddingProviderChange(value) {
1555
+ var textVal = document.getElementById('select-text-provider').value;
1556
+ // Show the OpenAI embedding model field only when embedding=openai
1557
+ document.getElementById('embed-fields-openai').style.display = value === 'openai' ? '' : 'none';
1558
+ refreshAnthropicWarning(textVal, value);
1559
+ saveBootSetting('embedding_provider', value);
1560
+ }
1561
+
1562
+ // Shows/hides the Anthropic+auto warning.
1563
+ // Warning appears when: text=anthropic AND embedding=auto (auto-bridges to Gemini).
1564
+ function refreshAnthropicWarning(textVal, embedVal) {
1565
+ var show = textVal === 'anthropic' && embedVal === 'auto';
1566
+ document.getElementById('anthropic-embed-warning').style.display = show ? '' : 'none';
1567
+ }
1568
+
1569
+ // Load all AI provider settings from the API and populate fields.
1570
+ // Called lazily when the tab is first activated (not on every modal open).
1571
+ async function loadAiProviderSettings() {
1572
+ try {
1573
+ var res = await fetch('/api/settings');
1574
+ var data = await res.json();
1575
+ var s = data.settings || {};
1576
+
1577
+ // ── Text provider dropdown ────────────────────────────────────────
1578
+ var textProvider = s.text_provider || 'gemini';
1579
+ var textSel = document.getElementById('select-text-provider');
1580
+ if (textSel) textSel.value = textProvider;
1581
+ document.getElementById('provider-fields-gemini').style.display = textProvider === 'gemini' ? '' : 'none';
1582
+ document.getElementById('provider-fields-openai').style.display = textProvider === 'openai' ? '' : 'none';
1583
+ document.getElementById('provider-fields-anthropic').style.display = textProvider === 'anthropic' ? '' : 'none';
1584
+
1585
+ // ── Embedding provider dropdown ───────────────────────────────────
1586
+ var embedProvider = s.embedding_provider || 'auto';
1587
+ var embedSel = document.getElementById('select-embedding-provider');
1588
+ if (embedSel) embedSel.value = embedProvider;
1589
+ document.getElementById('embed-fields-openai').style.display = embedProvider === 'openai' ? '' : 'none';
1590
+ refreshAnthropicWarning(textProvider, embedProvider);
1591
+
1592
+ // ── Gemini fields ─────────────────────────────────────────────────
1593
+ // Never pre-fill API key values for security — use placeholder hint instead.
1594
+ var gKey = document.getElementById('input-google-api-key');
1595
+ if (gKey) gKey.placeholder = s.GOOGLE_API_KEY ? '(key saved — paste to update)' : 'AIza…';
1596
+
1597
+ // ── Anthropic fields ──────────────────────────────────────────────
1598
+ var aKey = document.getElementById('input-anthropic-api-key');
1599
+ if (aKey) aKey.placeholder = s.anthropic_api_key ? '(key saved — paste to update)' : 'sk-ant-…';
1600
+ var aMod = document.getElementById('input-anthropic-model');
1601
+ if (aMod && s.anthropic_model) aMod.value = s.anthropic_model;
1602
+
1603
+ // ── OpenAI / Ollama fields ────────────────────────────────────────
1604
+ var oKey = document.getElementById('input-openai-api-key');
1605
+ if (oKey) oKey.placeholder = s.openai_api_key ? '(key saved — paste to update)' : 'sk-… (blank for Ollama)';
1606
+ var oUrl = document.getElementById('input-openai-base-url');
1607
+ if (oUrl && s.openai_base_url) oUrl.value = s.openai_base_url;
1608
+ var oMod = document.getElementById('input-openai-model');
1609
+ if (oMod && s.openai_model) oMod.value = s.openai_model;
1610
+ var oEmb = document.getElementById('input-openai-embedding-model');
1611
+ if (oEmb && s.openai_embedding_model) oEmb.value = s.openai_embedding_model;
1612
+
1613
+ } catch(e) { console.warn('AI provider settings load failed:', e); }
1614
+ }
1615
+
1616
+
1313
1617
 
1314
1618
  // ─── Auto-Load Checkboxes (v4.1) ─────────────────────────────────
1315
1619
  async function loadAutoloadCheckboxes() {
@@ -1424,9 +1728,36 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1424
1728
  loadAutoloadCheckboxes();
1425
1729
  // Repo path inputs are loaded dynamically
1426
1730
  loadRepoPathInputs();
1731
+ // OTel settings are loaded dynamically when the tab is first opened,
1732
+ // but also pre-load here so values are ready if user lands on that tab.
1733
+ loadOtelSettings();
1427
1734
  } catch(e) { console.warn('Settings load failed:', e); }
1428
1735
  }
1429
1736
 
1737
+ // ─── OTel Settings Hydration (v4.6.0) ────────────────────────────────
1738
+ // Separate loader function so it can be called from both loadSettings()
1739
+ // (pre-warm on modal open) and switchSettingsTab('observability')
1740
+ // (refresh on tab focus, in case settings changed elsewhere).
1741
+ async function loadOtelSettings() {
1742
+ try {
1743
+ var res = await fetch('/api/settings');
1744
+ var data = await res.json();
1745
+ var s = data.settings || {};
1746
+
1747
+ // Toggle: checked when otel_enabled === 'true'
1748
+ var enabledEl = document.getElementById('input-otel-enabled');
1749
+ if (enabledEl) enabledEl.checked = s.otel_enabled === 'true';
1750
+
1751
+ // OTLP endpoint: fall back to Jaeger default so the field is never blank
1752
+ var endpointEl = document.getElementById('input-otel-endpoint');
1753
+ if (endpointEl) endpointEl.value = s.otel_endpoint || 'http://localhost:4318/v1/traces';
1754
+
1755
+ // Service name: fall back to canonical default
1756
+ var serviceEl = document.getElementById('input-otel-service');
1757
+ if (serviceEl) serviceEl.value = s.otel_service_name || 'prism-mcp-server';
1758
+ } catch(e) { console.warn('OTel settings load failed:', e); }
1759
+ }
1760
+
1430
1761
  function toggleSetting(key, el) {
1431
1762
  var isActive = el.classList.toggle('active');
1432
1763
  saveSetting(key, isActive ? 'true' : 'false');
package/dist/lifecycle.js CHANGED
@@ -11,6 +11,7 @@ import * as os from "os";
11
11
  import { execSync } from "child_process";
12
12
  import { closeConfigStorage } from "./storage/configStorage.js";
13
13
  import { getStorage } from "./storage/index.js";
14
+ import { shutdownTelemetry } from "./utils/telemetry.js";
14
15
  const PRISM_DIR = path.join(os.homedir(), ".prism-mcp");
15
16
  /**
16
17
  * Instance-aware PID file.
@@ -126,6 +127,11 @@ export function registerShutdownHandlers() {
126
127
  shuttingDown = true;
127
128
  log(`Shutting down gracefully (${reason})...`);
128
129
  try {
130
+ // 0. Flush OTel span buffer FIRST — before any DBs are closed.
131
+ // BatchSpanProcessor holds spans in memory (up to 5s). If we close
132
+ // DBs first, spans that reference DB operations lose their context.
133
+ // shutdownTelemetry() is a no-op when otel_enabled=false.
134
+ await shutdownTelemetry();
129
135
  // 1. Close system settings DB
130
136
  closeConfigStorage();
131
137
  // 2. Close main ledger DB