@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.
- package/dist/chunk-CYDXIW4L.js +38 -0
- package/dist/chunk-CYDXIW4L.js.map +1 -0
- package/dist/{chunk-ODBRN4NR.js → chunk-YAXXO3X4.js} +167 -45
- package/dist/chunk-YAXXO3X4.js.map +1 -0
- package/dist/dashboard.js +28 -24
- package/dist/dashboard.js.map +1 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/standalone.js +31 -19
- package/dist/standalone.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ODBRN4NR.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2106
|
+
safeLog.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
|
|
1991
2107
|
} catch (err) {
|
|
1992
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
7535
|
+
//# sourceMappingURL=chunk-YAXXO3X4.js.map
|