@runtimescope/collector 0.10.7 → 0.10.9

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.
@@ -0,0 +1,38 @@
1
+ // src/log.ts
2
+ function formatArg(a) {
3
+ if (typeof a === "string") return a;
4
+ if (a instanceof Error) return a.stack ?? a.message;
5
+ if (a === null || a === void 0) return String(a);
6
+ if (typeof a === "object") {
7
+ try {
8
+ return JSON.stringify(a);
9
+ } catch {
10
+ return String(a);
11
+ }
12
+ }
13
+ return String(a);
14
+ }
15
+ function write(stream, args) {
16
+ try {
17
+ if (!stream.writable) {
18
+ process.exit(1);
19
+ }
20
+ const formatted = args.map(formatArg).join(" ");
21
+ stream.write(formatted + "\n");
22
+ } catch {
23
+ process.exit(1);
24
+ }
25
+ }
26
+ var safeLog = {
27
+ error(...args) {
28
+ write(process.stderr, args);
29
+ },
30
+ warn(...args) {
31
+ write(process.stderr, args);
32
+ }
33
+ };
34
+
35
+ export {
36
+ safeLog
37
+ };
38
+ //# sourceMappingURL=chunk-CYDXIW4L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/log.ts"],"sourcesContent":["// ============================================================\n// safeLog — EPIPE-resilient stderr writes\n//\n// Why this exists:\n// v0.10.8 fixed a class of bug where the npx-spawned MCP server got\n// reparented to init when Claude Code exited, then entered a tight\n// uncaughtException → console.error → uncaughtException loop against\n// a closed stderr pipe. The fix wrapped the two handler entry points,\n// but the codebase has 100+ raw `console.error` / `process.stderr.write`\n// sites in ordinary code paths (PM discovery, otel exporter, WAL\n// recovery, etc.). Any one of those can re-trigger the same loop if\n// it fires while stderr is broken — the v0.10.8 fix is necessary but\n// not sufficient.\n//\n// See audit: docs/audits/0001-collector-process-lifetime.md F1\n// See ADR: docs/decisions/0001-audit-then-rust.md\n//\n// Contract:\n// - Synchronous writes to stderr, formatted similarly to console.error.\n// - If stderr is unwritable (parent died, pipe closed), the call exits\n// the process with code 1 rather than throwing. We cannot meaningfully\n// surface anything to a dead pipe; the only behavior that doesn't\n// cascade into a CPU-pegged loop is to bail.\n// - Drop-in replacement for console.error / console.warn — supports\n// multi-arg format (`safeLog.error('foo:', err.message)`).\n// - Never throws; never re-enters. Safe to call from inside an\n// uncaughtException handler.\n// ============================================================\n\nfunction formatArg(a: unknown): string {\n if (typeof a === 'string') return a;\n if (a instanceof Error) return a.stack ?? a.message;\n if (a === null || a === undefined) return String(a);\n if (typeof a === 'object') {\n try {\n return JSON.stringify(a);\n } catch {\n // Circular refs or non-serializable values — fall back to toString.\n return String(a);\n }\n }\n return String(a);\n}\n\nfunction write(stream: NodeJS.WriteStream, args: unknown[]): void {\n try {\n if (!stream.writable) {\n // Pipe is gone. Don't try to log anything else, just exit.\n // We use code 1 to distinguish \"I exited because my stderr broke\"\n // from \"I exited because my stdin closed\" (which uses code 0).\n process.exit(1);\n }\n const formatted = args.map(formatArg).join(' ');\n stream.write(formatted + '\\n');\n } catch {\n // The write itself threw (EPIPE landed between the writable check and\n // the write). Same conclusion: exit, do not loop.\n process.exit(1);\n }\n}\n\nexport const safeLog = {\n error(...args: unknown[]): void {\n write(process.stderr, args);\n },\n warn(...args: unknown[]): void {\n write(process.stderr, args);\n },\n};\n"],"mappings":";AA6BA,SAAS,UAAU,GAAoB;AACrC,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,aAAa,MAAO,QAAO,EAAE,SAAS,EAAE;AAC5C,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO,OAAO,CAAC;AAClD,MAAI,OAAO,MAAM,UAAU;AACzB,QAAI;AACF,aAAO,KAAK,UAAU,CAAC;AAAA,IACzB,QAAQ;AAEN,aAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AACA,SAAO,OAAO,CAAC;AACjB;AAEA,SAAS,MAAM,QAA4B,MAAuB;AAChE,MAAI;AACF,QAAI,CAAC,OAAO,UAAU;AAIpB,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM,YAAY,KAAK,IAAI,SAAS,EAAE,KAAK,GAAG;AAC9C,WAAO,MAAM,YAAY,IAAI;AAAA,EAC/B,QAAQ;AAGN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEO,IAAM,UAAU;AAAA,EACrB,SAAS,MAAuB;AAC9B,UAAM,QAAQ,QAAQ,IAAI;AAAA,EAC5B;AAAA,EACA,QAAQ,MAAuB;AAC7B,UAAM,QAAQ,QAAQ,IAAI;AAAA,EAC5B;AACF;","names":[]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ safeLog
3
+ } from "./chunk-CYDXIW4L.js";
1
4
  import {
2
5
  __require
3
6
  } from "./chunk-UP2VWCW5.js";
@@ -76,6 +79,18 @@ var EventStore = class {
76
79
  this.sqliteStore = store;
77
80
  this.currentProject = project;
78
81
  }
82
+ /**
83
+ * Drop the SQLite pointer if it matches the given project. Called by the
84
+ * collector's idle-store eviction timer so the store doesn't try to write
85
+ * through a closed handle after eviction. The next `setSqliteStore` (from
86
+ * the next event for this project) restores the binding.
87
+ */
88
+ clearSqliteStoreIfMatches(project) {
89
+ if (this.currentProject === project) {
90
+ this.sqliteStore = null;
91
+ this.currentProject = null;
92
+ }
93
+ }
79
94
  /**
80
95
  * Pre-load recent events from a SqliteStore into the in-memory ring buffer.
81
96
  * Used at collector startup to make MCP tools immediately useful after a
@@ -471,6 +486,7 @@ var SqliteStore = class _SqliteStore {
471
486
  this.db = this.openDatabase(options);
472
487
  const flushInterval = options.flushIntervalMs ?? 100;
473
488
  this.flushTimer = setInterval(() => this.flush(), flushInterval);
489
+ this.flushTimer.unref?.();
474
490
  }
475
491
  openDatabase(options) {
476
492
  const Db = getDatabase();
@@ -488,14 +504,14 @@ var SqliteStore = class _SqliteStore {
488
504
  this.prepareStatements(db);
489
505
  return db;
490
506
  } catch (err) {
491
- console.error(
507
+ safeLog.error(
492
508
  `[RuntimeScope] SQLite database corrupt or unreadable (${err.message}), recreating...`
493
509
  );
494
510
  try {
495
511
  if (existsSync(options.dbPath)) {
496
512
  const backupPath = `${options.dbPath}.corrupt.${Date.now()}`;
497
513
  renameSync(options.dbPath, backupPath);
498
- console.error(`[RuntimeScope] Renamed corrupt DB to ${backupPath}`);
514
+ safeLog.error(`[RuntimeScope] Renamed corrupt DB to ${backupPath}`);
499
515
  }
500
516
  for (const suffix of ["-wal", "-shm"]) {
501
517
  const p = options.dbPath + suffix;
@@ -614,7 +630,7 @@ var SqliteStore = class _SqliteStore {
614
630
  try {
615
631
  insertMany();
616
632
  } catch (err) {
617
- console.error("[RuntimeScope] SQLite flush error:", err.message);
633
+ safeLog.error("[RuntimeScope] SQLite flush error:", err.message);
618
634
  }
619
635
  }
620
636
  saveSession(info) {
@@ -878,7 +894,7 @@ function isSqliteAvailable() {
878
894
  _available = true;
879
895
  } catch {
880
896
  _available = false;
881
- console.error(
897
+ safeLog.error(
882
898
  "[RuntimeScope] better-sqlite3 is not available \u2014 running in memory-only mode.\n[RuntimeScope] Historical data persistence is disabled. To fix this:\n[RuntimeScope] macOS: xcode-select --install\n[RuntimeScope] Ubuntu: sudo apt-get install build-essential python3\n[RuntimeScope] Windows: npm install --global windows-build-tools\n[RuntimeScope] Then run: npm rebuild better-sqlite3"
883
899
  );
884
900
  }
@@ -1491,7 +1507,7 @@ var OtelExporter = class {
1491
1507
  const now = Date.now();
1492
1508
  if (now - this.lastFailureLog < 6e4) return;
1493
1509
  this.lastFailureLog = now;
1494
- console.error(`[RuntimeScope] ${msg}`);
1510
+ safeLog.error(`[RuntimeScope] ${msg}`);
1495
1511
  }
1496
1512
  };
1497
1513
  function traceIdFromSession(sessionId) {
@@ -1632,7 +1648,7 @@ var SessionRateLimiter = class {
1632
1648
  maybeWarn(sessionId, w, now) {
1633
1649
  if (now - w.lastWarning >= 6e4) {
1634
1650
  w.lastWarning = now;
1635
- console.error(
1651
+ safeLog.error(
1636
1652
  `[RuntimeScope] Rate limiting session ${sessionId.slice(0, 8)}... (dropped ${this._droppedTotal} total)`
1637
1653
  );
1638
1654
  }
@@ -1674,7 +1690,23 @@ var CollectorServer = class {
1674
1690
  pendingHandshakes = /* @__PURE__ */ new Set();
1675
1691
  pendingCommands = /* @__PURE__ */ new Map();
1676
1692
  sqliteStores = /* @__PURE__ */ new Map();
1693
+ // Per-project last-access timestamps for the SQLite stores. Used by the
1694
+ // LRU eviction timer to close handles for projects that haven't been read
1695
+ // from or written to recently — without this, every project ever seen
1696
+ // keeps its WAL handle + ~2-3MB page cache open forever, which on a
1697
+ // 40-project machine adds up to ~100MB of permanently-allocated baseline
1698
+ // RSS. ensureSqliteStore() updates this on every access.
1699
+ sqliteStoreLastAccess = /* @__PURE__ */ new Map();
1677
1700
  wals = /* @__PURE__ */ new Map();
1701
+ // Per-project last-access timestamps for WAL handles, mirroring
1702
+ // sqliteStoreLastAccess. The audit (docs/audits/0001 §F3) found that
1703
+ // WAL handles followed the same "open on first use, only closed on
1704
+ // stop()" lifetime as the SQLite stores did pre-v0.10.8 — fewer bytes
1705
+ // per handle than SQLite, but each holds an open FD, so on machines
1706
+ // with many projects the leak is real (ulimit risk + small RSS drift).
1707
+ // ensureWal() updates this on every access; the sqliteEvictTimer sweep
1708
+ // evicts idle handles in the same pass.
1709
+ walsLastAccess = /* @__PURE__ */ new Map();
1678
1710
  ready = false;
1679
1711
  metrics = new MetricsRegistry();
1680
1712
  startedAt = Date.now();
@@ -1684,6 +1716,7 @@ var CollectorServer = class {
1684
1716
  disconnectCallbacks = [];
1685
1717
  pruneTimer = null;
1686
1718
  heartbeatTimer = null;
1719
+ sqliteEvictTimer = null;
1687
1720
  tlsConfig = null;
1688
1721
  pmStore = null;
1689
1722
  constructor(options = {}) {
@@ -1697,6 +1730,7 @@ var CollectorServer = class {
1697
1730
  }
1698
1731
  if (this.rateLimiter.isEnabled()) {
1699
1732
  this.pruneTimer = setInterval(() => this.rateLimiter.prune(), 6e4);
1733
+ this.pruneTimer.unref?.();
1700
1734
  }
1701
1735
  this.counters = {
1702
1736
  eventsTotal: this.metrics.counter(
@@ -1754,7 +1788,7 @@ var CollectorServer = class {
1754
1788
  this.store.onEvent((event) => {
1755
1789
  this.otelExporter?.ingest(event);
1756
1790
  });
1757
- console.error(
1791
+ safeLog.error(
1758
1792
  `[RuntimeScope] OpenTelemetry export enabled \u2192 ${otelOptions.endpoint}`
1759
1793
  );
1760
1794
  }
@@ -1782,6 +1816,24 @@ var CollectorServer = class {
1782
1816
  getSqliteStores() {
1783
1817
  return this.sqliteStores;
1784
1818
  }
1819
+ /**
1820
+ * Diagnostic counters for currently-open per-project resource handles
1821
+ * and pending bidirectional commands. Exposed for tests + future
1822
+ * tray-app health surfaces.
1823
+ *
1824
+ * - `sqliteStores` / `wals`: open handle counts. The eviction sweep
1825
+ * (5-minute idle in production, env-overridable for tests) drives
1826
+ * these down to zero for idle projects and back up on reconnect.
1827
+ * - `pendingCommands`: in-flight WS commands (server → SDK). Settles
1828
+ * on response or via the per-command timeout — see audit F5.
1829
+ */
1830
+ getOpenHandleCounts() {
1831
+ return {
1832
+ sqliteStores: this.sqliteStores.size,
1833
+ wals: this.wals.size,
1834
+ pendingCommands: this.pendingCommands.size
1835
+ };
1836
+ }
1785
1837
  getRateLimiter() {
1786
1838
  return this.rateLimiter;
1787
1839
  }
@@ -1828,7 +1880,7 @@ var CollectorServer = class {
1828
1880
  sqliteBytes = sqliteStore.snapshotTo(sqlitePath);
1829
1881
  eventCount = sqliteStore.getEventCount({ project: projectName });
1830
1882
  } catch (err) {
1831
- console.error(
1883
+ safeLog.error(
1832
1884
  `[RuntimeScope] Snapshot of "${projectName}" SQLite failed:`,
1833
1885
  err.message
1834
1886
  );
@@ -1840,7 +1892,7 @@ var CollectorServer = class {
1840
1892
  try {
1841
1893
  walBytes = wal.snapshotTo(join2(projectDir, "wal"));
1842
1894
  } catch (err) {
1843
- console.error(
1895
+ safeLog.error(
1844
1896
  `[RuntimeScope] Snapshot of "${projectName}" WAL failed:`,
1845
1897
  err.message
1846
1898
  );
@@ -1869,12 +1921,75 @@ var CollectorServer = class {
1869
1921
  try {
1870
1922
  this.runStartupRecovery();
1871
1923
  } catch (err) {
1872
- console.error("[RuntimeScope] Startup recovery failed (non-fatal):", err.message);
1924
+ safeLog.error("[RuntimeScope] Startup recovery failed (non-fatal):", err.message);
1873
1925
  }
1874
1926
  }
1875
1927
  this.ready = true;
1928
+ this.startSqliteEvictionTimer();
1876
1929
  return this.tryStart(port, host, maxRetries, retryDelayMs, tls);
1877
1930
  }
1931
+ /**
1932
+ * Close SQLite stores that haven't been accessed in IDLE_TIMEOUT_MS. Any
1933
+ * subsequent `ensureSqliteStore(name)` call will transparently re-open the
1934
+ * store from disk, so eviction is invisible to callers — they just see a
1935
+ * brief warm-up the next time they touch a project.
1936
+ *
1937
+ * We deliberately do NOT evict stores belonging to currently-connected
1938
+ * SDK clients, since those are guaranteed to write again soon and closing
1939
+ * + reopening on every event would be expensive.
1940
+ */
1941
+ startSqliteEvictionTimer() {
1942
+ if (this.sqliteEvictTimer) return;
1943
+ const IDLE_TIMEOUT_MS = parseInt(
1944
+ process.env.RUNTIMESCOPE_SQLITE_IDLE_MS ?? String(5 * 60 * 1e3),
1945
+ 10
1946
+ );
1947
+ const SWEEP_INTERVAL_MS = parseInt(
1948
+ process.env.RUNTIMESCOPE_SQLITE_SWEEP_MS ?? String(60 * 1e3),
1949
+ 10
1950
+ );
1951
+ this.sqliteEvictTimer = setInterval(() => {
1952
+ const now = Date.now();
1953
+ const liveProjects = /* @__PURE__ */ new Set();
1954
+ for (const info of this.clients.values()) {
1955
+ liveProjects.add(info.projectName);
1956
+ }
1957
+ for (const [projectName, store] of this.sqliteStores) {
1958
+ if (liveProjects.has(projectName)) continue;
1959
+ const lastAccess = this.sqliteStoreLastAccess.get(projectName) ?? 0;
1960
+ if (now - lastAccess < IDLE_TIMEOUT_MS) continue;
1961
+ try {
1962
+ store.close();
1963
+ } catch (e) {
1964
+ safeLog.error(
1965
+ `[RuntimeScope] Failed to close idle SQLite store "${projectName}":`,
1966
+ e.message
1967
+ );
1968
+ continue;
1969
+ }
1970
+ this.sqliteStores.delete(projectName);
1971
+ this.sqliteStoreLastAccess.delete(projectName);
1972
+ this.store.clearSqliteStoreIfMatches(projectName);
1973
+ }
1974
+ for (const [projectName, wal] of this.wals) {
1975
+ if (liveProjects.has(projectName)) continue;
1976
+ const lastAccess = this.walsLastAccess.get(projectName) ?? 0;
1977
+ if (now - lastAccess < IDLE_TIMEOUT_MS) continue;
1978
+ try {
1979
+ wal.close();
1980
+ } catch (e) {
1981
+ safeLog.error(
1982
+ `[RuntimeScope] Failed to close idle WAL "${projectName}":`,
1983
+ e.message
1984
+ );
1985
+ continue;
1986
+ }
1987
+ this.wals.delete(projectName);
1988
+ this.walsLastAccess.delete(projectName);
1989
+ }
1990
+ }, SWEEP_INTERVAL_MS);
1991
+ this.sqliteEvictTimer.unref?.();
1992
+ }
1878
1993
  /**
1879
1994
  * On collector startup, for each known project:
1880
1995
  * 1. Replay any sealed/active WAL files into SqliteStore (mirror of the
@@ -1923,7 +2038,7 @@ var CollectorServer = class {
1923
2038
  }
1924
2039
  }
1925
2040
  if (walReplayed > 0 || warmed > 0) {
1926
- console.error(
2041
+ safeLog.error(
1927
2042
  `[RuntimeScope] Recovery: ${walReplayed} WAL events replayed, ${warmed} events warmed into ring buffer.`
1928
2043
  );
1929
2044
  }
@@ -1939,7 +2054,7 @@ var CollectorServer = class {
1939
2054
  this.setupConnectionHandler(wss);
1940
2055
  this.setupPersistentErrorHandler(wss);
1941
2056
  this.startHeartbeat(wss);
1942
- console.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
2057
+ safeLog.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
1943
2058
  resolve2();
1944
2059
  });
1945
2060
  httpsServer.on("error", (err) => {
@@ -1954,7 +2069,7 @@ var CollectorServer = class {
1954
2069
  this.setupConnectionHandler(wss);
1955
2070
  this.setupPersistentErrorHandler(wss);
1956
2071
  this.startHeartbeat(wss);
1957
- console.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
2072
+ safeLog.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
1958
2073
  resolve2();
1959
2074
  });
1960
2075
  wss.on("error", (err) => {
@@ -1967,18 +2082,19 @@ var CollectorServer = class {
1967
2082
  handleStartError(err, port, host, retriesLeft, retryDelayMs, tls, resolve2, reject) {
1968
2083
  if (err.code === "EADDRINUSE" && retriesLeft > 0) {
1969
2084
  const nextPort = port + 1;
1970
- console.error(
2085
+ safeLog.error(
1971
2086
  `[RuntimeScope] Port ${port} in use, trying ${nextPort}...`
1972
2087
  );
1973
2088
  this.tryStart(nextPort, host, retriesLeft - 1, retryDelayMs, tls).then(resolve2).catch(reject);
1974
2089
  } else {
1975
- console.error("[RuntimeScope] WebSocket server error:", err.message);
2090
+ safeLog.error("[RuntimeScope] WebSocket server error:", err.message);
1976
2091
  reject(err);
1977
2092
  }
1978
2093
  }
1979
2094
  ensureSqliteStore(projectName) {
1980
2095
  if (!this.projectManager) return null;
1981
2096
  if (!isSqliteAvailable()) return null;
2097
+ this.sqliteStoreLastAccess.set(projectName, Date.now());
1982
2098
  let sqliteStore = this.sqliteStores.get(projectName);
1983
2099
  if (!sqliteStore) {
1984
2100
  try {
@@ -1987,9 +2103,9 @@ var CollectorServer = class {
1987
2103
  sqliteStore = new SqliteStore({ dbPath });
1988
2104
  this.sqliteStores.set(projectName, sqliteStore);
1989
2105
  this.store.setSqliteStore(sqliteStore, projectName);
1990
- console.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
2106
+ safeLog.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
1991
2107
  } catch (err) {
1992
- console.error(
2108
+ safeLog.error(
1993
2109
  `[RuntimeScope] Failed to open SQLite for "${projectName}":`,
1994
2110
  err.message
1995
2111
  );
@@ -2016,6 +2132,7 @@ var CollectorServer = class {
2016
2132
  */
2017
2133
  ensureWal(projectName) {
2018
2134
  if (!this.projectManager) return null;
2135
+ this.walsLastAccess.set(projectName, Date.now());
2019
2136
  let wal = this.wals.get(projectName);
2020
2137
  if (wal) return wal;
2021
2138
  const dir = this.walDirFor(projectName);
@@ -2026,7 +2143,7 @@ var CollectorServer = class {
2026
2143
  this.wals.set(projectName, wal);
2027
2144
  return wal;
2028
2145
  } catch (err) {
2029
- console.error(
2146
+ safeLog.error(
2030
2147
  `[RuntimeScope] Failed to open WAL for "${projectName}":`,
2031
2148
  err.message
2032
2149
  );
@@ -2063,7 +2180,7 @@ var CollectorServer = class {
2063
2180
  if (sqliteStore && replayed > 0) {
2064
2181
  sqliteStore.flush();
2065
2182
  for (const file of files) Wal.deleteSealed(file);
2066
- console.error(
2183
+ safeLog.error(
2067
2184
  `[RuntimeScope] WAL recovery: replayed ${replayed} events for "${projectName}"`
2068
2185
  );
2069
2186
  }
@@ -2083,7 +2200,7 @@ var CollectorServer = class {
2083
2200
  /** Catch runtime errors on the WSS so an unhandled error doesn't crash the process */
2084
2201
  setupPersistentErrorHandler(wss) {
2085
2202
  wss.on("error", (err) => {
2086
- console.error("[RuntimeScope] WebSocket server runtime error:", err.message);
2203
+ safeLog.error("[RuntimeScope] WebSocket server runtime error:", err.message);
2087
2204
  });
2088
2205
  }
2089
2206
  /** Ping all connected clients every 15s — terminate those that don't respond */
@@ -2099,6 +2216,7 @@ var CollectorServer = class {
2099
2216
  ws.ping();
2100
2217
  }
2101
2218
  }, 15e3);
2219
+ this.heartbeatTimer.unref?.();
2102
2220
  }
2103
2221
  setupConnectionHandler(wss) {
2104
2222
  wss.on("connection", (ws) => {
@@ -2133,7 +2251,7 @@ var CollectorServer = class {
2133
2251
  const msg = JSON.parse(data.toString());
2134
2252
  this.handleMessage(ws, msg);
2135
2253
  } catch {
2136
- console.error("[RuntimeScope] Malformed WebSocket message, ignoring");
2254
+ safeLog.error("[RuntimeScope] Malformed WebSocket message, ignoring");
2137
2255
  }
2138
2256
  });
2139
2257
  ws.on("close", (code) => {
@@ -2146,7 +2264,7 @@ var CollectorServer = class {
2146
2264
  if (sqliteStore) {
2147
2265
  sqliteStore.updateSessionDisconnected(clientInfo.sessionId, Date.now());
2148
2266
  }
2149
- console.error(`[RuntimeScope] Session ${clientInfo.sessionId} disconnected`);
2267
+ safeLog.error(`[RuntimeScope] Session ${clientInfo.sessionId} disconnected`);
2150
2268
  for (const cb of this.disconnectCallbacks) {
2151
2269
  try {
2152
2270
  cb(clientInfo.sessionId, clientInfo.projectName, clientInfo.projectId);
@@ -2157,7 +2275,7 @@ var CollectorServer = class {
2157
2275
  this.clients.delete(ws);
2158
2276
  });
2159
2277
  ws.on("error", (err) => {
2160
- console.error("[RuntimeScope] WebSocket client error:", err.message);
2278
+ safeLog.error("[RuntimeScope] WebSocket client error:", err.message);
2161
2279
  });
2162
2280
  });
2163
2281
  }
@@ -2231,7 +2349,7 @@ var CollectorServer = class {
2231
2349
  connectedAt: msg.timestamp,
2232
2350
  sdkVersion: payload.sdkVersion
2233
2351
  });
2234
- console.error(
2352
+ safeLog.error(
2235
2353
  `[RuntimeScope] Session ${payload.sessionId} connected (${payload.appName} v${payload.sdkVersion})`
2236
2354
  );
2237
2355
  for (const cb of this.connectCallbacks) {
@@ -2266,7 +2384,7 @@ var CollectorServer = class {
2266
2384
  wal.append(accepted);
2267
2385
  wal.commit();
2268
2386
  } catch (err) {
2269
- console.error("[RuntimeScope] WAL append/commit failed:", err.message);
2387
+ safeLog.error("[RuntimeScope] WAL append/commit failed:", err.message);
2270
2388
  this.counters.eventsDropped.inc(accepted.length, { reason: "wal_backpressure" });
2271
2389
  }
2272
2390
  }
@@ -2277,7 +2395,7 @@ var CollectorServer = class {
2277
2395
  try {
2278
2396
  this.checkpointWal(clientInfo.projectName, wal);
2279
2397
  } catch (err) {
2280
- console.error("[RuntimeScope] WAL checkpoint failed:", err.message);
2398
+ safeLog.error("[RuntimeScope] WAL checkpoint failed:", err.message);
2281
2399
  }
2282
2400
  }
2283
2401
  break;
@@ -2361,6 +2479,10 @@ var CollectorServer = class {
2361
2479
  clearInterval(this.pruneTimer);
2362
2480
  this.pruneTimer = null;
2363
2481
  }
2482
+ if (this.sqliteEvictTimer) {
2483
+ clearInterval(this.sqliteEvictTimer);
2484
+ this.sqliteEvictTimer = null;
2485
+ }
2364
2486
  if (this.wss) {
2365
2487
  for (const client of this.wss.clients) {
2366
2488
  if (client.readyState === 1) {
@@ -2385,14 +2507,14 @@ var CollectorServer = class {
2385
2507
  try {
2386
2508
  wal.close();
2387
2509
  } catch {
2388
- console.error(`[RuntimeScope] WAL close error for "${name}" (non-fatal)`);
2510
+ safeLog.error(`[RuntimeScope] WAL close error for "${name}" (non-fatal)`);
2389
2511
  }
2390
2512
  }
2391
2513
  this.wals.clear();
2392
2514
  for (const [name, sqliteStore] of this.sqliteStores) {
2393
2515
  try {
2394
2516
  sqliteStore.close();
2395
- console.error(`[RuntimeScope] SQLite store closed for "${name}"`);
2517
+ safeLog.error(`[RuntimeScope] SQLite store closed for "${name}"`);
2396
2518
  } catch {
2397
2519
  }
2398
2520
  }
@@ -2400,7 +2522,7 @@ var CollectorServer = class {
2400
2522
  if (this.wss) {
2401
2523
  this.wss.close();
2402
2524
  this.wss = null;
2403
- console.error("[RuntimeScope] Collector stopped");
2525
+ safeLog.error("[RuntimeScope] Collector stopped");
2404
2526
  }
2405
2527
  this.ready = false;
2406
2528
  }
@@ -4975,7 +5097,7 @@ var HttpServer = class {
4975
5097
  } catch (err) {
4976
5098
  const isAddrInUse = err.code === "EADDRINUSE";
4977
5099
  if (isAddrInUse && attempt < maxRetries) {
4978
- console.error(`[RuntimeScope] HTTP port ${port} in use, trying ${port + 1}...`);
5100
+ safeLog.error(`[RuntimeScope] HTTP port ${port} in use, trying ${port + 1}...`);
4979
5101
  continue;
4980
5102
  }
4981
5103
  throw err;
@@ -5009,7 +5131,7 @@ var HttpServer = class {
5009
5131
  this.activePort = boundPort;
5010
5132
  this.startedAt = Date.now();
5011
5133
  const proto = tls ? "https" : "http";
5012
- console.error(`[RuntimeScope] HTTP API listening on ${proto}://${host}:${boundPort}`);
5134
+ safeLog.error(`[RuntimeScope] HTTP API listening on ${proto}://${host}:${boundPort}`);
5013
5135
  resolve2();
5014
5136
  });
5015
5137
  server.on("error", (err) => {
@@ -5041,7 +5163,7 @@ var HttpServer = class {
5041
5163
  return new Promise((resolve2) => {
5042
5164
  this.server.close(() => {
5043
5165
  this.server = null;
5044
- console.error("[RuntimeScope] HTTP API stopped");
5166
+ safeLog.error("[RuntimeScope] HTTP API stopped");
5045
5167
  resolve2();
5046
5168
  });
5047
5169
  });
@@ -6924,7 +7046,7 @@ var ProjectDiscovery = class {
6924
7046
  return mergeResults(claudeResult, runtimeResult);
6925
7047
  } catch (err) {
6926
7048
  const msg = err instanceof Error ? err.message : String(err);
6927
- console.error(`${LOG_PREFIX} Fatal discovery error: ${msg}`);
7049
+ safeLog.error(`${LOG_PREFIX} Fatal discovery error: ${msg}`);
6928
7050
  result.errors.push(`Fatal discovery error: ${msg}`);
6929
7051
  return result;
6930
7052
  }
@@ -6952,7 +7074,7 @@ var ProjectDiscovery = class {
6952
7074
  entries = dirEntries.filter((d) => d.isDirectory()).map((d) => d.name);
6953
7075
  } catch (err) {
6954
7076
  const msg = err instanceof Error ? err.message : String(err);
6955
- console.error(`${LOG_PREFIX} Failed to read Claude projects dir: ${msg}`);
7077
+ safeLog.error(`${LOG_PREFIX} Failed to read Claude projects dir: ${msg}`);
6956
7078
  result.errors.push(`Failed to read Claude projects dir: ${msg}`);
6957
7079
  return result;
6958
7080
  }
@@ -6961,7 +7083,7 @@ var ProjectDiscovery = class {
6961
7083
  await this.processClaudeProject(key, result);
6962
7084
  } catch (err) {
6963
7085
  const msg = err instanceof Error ? err.message : String(err);
6964
- console.error(`${LOG_PREFIX} Error processing Claude project ${key}: ${msg}`);
7086
+ safeLog.error(`${LOG_PREFIX} Error processing Claude project ${key}: ${msg}`);
6965
7087
  result.errors.push(`Claude project ${key}: ${msg}`);
6966
7088
  }
6967
7089
  }
@@ -7028,13 +7150,13 @@ var ProjectDiscovery = class {
7028
7150
  }
7029
7151
  } catch (err) {
7030
7152
  const msg = err instanceof Error ? err.message : String(err);
7031
- console.error(`${LOG_PREFIX} Error processing RuntimeScope project ${projectName}: ${msg}`);
7153
+ safeLog.error(`${LOG_PREFIX} Error processing RuntimeScope project ${projectName}: ${msg}`);
7032
7154
  result.errors.push(`RuntimeScope project ${projectName}: ${msg}`);
7033
7155
  }
7034
7156
  }
7035
7157
  } catch (err) {
7036
7158
  const msg = err instanceof Error ? err.message : String(err);
7037
- console.error(`${LOG_PREFIX} Failed to list RuntimeScope projects: ${msg}`);
7159
+ safeLog.error(`${LOG_PREFIX} Failed to list RuntimeScope projects: ${msg}`);
7038
7160
  result.errors.push(`Failed to list RuntimeScope projects: ${msg}`);
7039
7161
  }
7040
7162
  return result;
@@ -7047,7 +7169,7 @@ var ProjectDiscovery = class {
7047
7169
  const existingProjects = await this.pmStore.listProjects();
7048
7170
  const project = existingProjects.find((p) => p.id === projectId);
7049
7171
  if (!project) {
7050
- console.error(`${LOG_PREFIX} Project not found: ${projectId}`);
7172
+ safeLog.error(`${LOG_PREFIX} Project not found: ${projectId}`);
7051
7173
  return 0;
7052
7174
  }
7053
7175
  if (!project.claudeProjectKey) {
@@ -7088,12 +7210,12 @@ var ProjectDiscovery = class {
7088
7210
  sessionsIndexed++;
7089
7211
  } catch (err) {
7090
7212
  const msg = err instanceof Error ? err.message : String(err);
7091
- console.error(`${LOG_PREFIX} Error indexing session ${jsonlFile}: ${msg}`);
7213
+ safeLog.error(`${LOG_PREFIX} Error indexing session ${jsonlFile}: ${msg}`);
7092
7214
  }
7093
7215
  }
7094
7216
  } catch (err) {
7095
7217
  const msg = err instanceof Error ? err.message : String(err);
7096
- console.error(`${LOG_PREFIX} Error indexing sessions for project ${projectId}: ${msg}`);
7218
+ safeLog.error(`${LOG_PREFIX} Error indexing sessions for project ${projectId}: ${msg}`);
7097
7219
  }
7098
7220
  return sessionsIndexed;
7099
7221
  }
@@ -7211,12 +7333,12 @@ var ProjectDiscovery = class {
7211
7333
  }
7212
7334
  } catch (err) {
7213
7335
  const msg = err instanceof Error ? err.message : String(err);
7214
- console.error(`${LOG_PREFIX} Error indexing session ${jsonlFile} in ${claudeKey}: ${msg}`);
7336
+ safeLog.error(`${LOG_PREFIX} Error indexing session ${jsonlFile} in ${claudeKey}: ${msg}`);
7215
7337
  }
7216
7338
  }
7217
7339
  } catch (err) {
7218
7340
  const msg = err instanceof Error ? err.message : String(err);
7219
- console.error(`${LOG_PREFIX} Error scanning sessions for ${claudeKey}: ${msg}`);
7341
+ safeLog.error(`${LOG_PREFIX} Error scanning sessions for ${claudeKey}: ${msg}`);
7220
7342
  }
7221
7343
  return counts;
7222
7344
  }
@@ -7310,7 +7432,7 @@ var ProjectDiscovery = class {
7310
7432
  };
7311
7433
  } catch (err) {
7312
7434
  const msg = err instanceof Error ? err.message : String(err);
7313
- console.error(`${LOG_PREFIX} Failed to parse session ${sessionId}: ${msg}`);
7435
+ safeLog.error(`${LOG_PREFIX} Failed to parse session ${sessionId}: ${msg}`);
7314
7436
  return {
7315
7437
  id: sessionId,
7316
7438
  projectId,
@@ -7360,7 +7482,7 @@ var ProjectDiscovery = class {
7360
7482
  await this.pmStore.upsertCapexEntry(entry);
7361
7483
  } catch (err) {
7362
7484
  const msg = err instanceof Error ? err.message : String(err);
7363
- console.error(`${LOG_PREFIX} Failed to create CapEx stub for session ${session.id}: ${msg}`);
7485
+ safeLog.error(`${LOG_PREFIX} Failed to create CapEx stub for session ${session.id}: ${msg}`);
7364
7486
  }
7365
7487
  }
7366
7488
  };
@@ -7410,4 +7532,4 @@ export {
7410
7532
  parseSessionJsonl,
7411
7533
  ProjectDiscovery
7412
7534
  };
7413
- //# sourceMappingURL=chunk-ODBRN4NR.js.map
7535
+ //# sourceMappingURL=chunk-YAXXO3X4.js.map