@memlab/mcp-server 2.1.1 → 2.18.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.
Files changed (175) hide show
  1. package/README.md +83 -3
  2. package/dist/analysis-budget.d.ts +71 -0
  3. package/dist/analysis-budget.d.ts.map +1 -0
  4. package/dist/analysis-budget.js +118 -0
  5. package/dist/analysis-budget.js.map +1 -0
  6. package/dist/guardrail.d.ts +24 -0
  7. package/dist/guardrail.d.ts.map +1 -0
  8. package/dist/guardrail.js +65 -0
  9. package/dist/guardrail.js.map +1 -0
  10. package/dist/heap-state.d.ts +53 -1
  11. package/dist/heap-state.d.ts.map +1 -1
  12. package/dist/heap-state.js +153 -10
  13. package/dist/heap-state.js.map +1 -1
  14. package/dist/index.js +38 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/tools/array-group-by.d.ts +12 -0
  17. package/dist/tools/array-group-by.d.ts.map +1 -0
  18. package/dist/tools/array-group-by.js +191 -0
  19. package/dist/tools/array-group-by.js.map +1 -0
  20. package/dist/tools/auto-investigate.d.ts.map +1 -1
  21. package/dist/tools/auto-investigate.js +1056 -52
  22. package/dist/tools/auto-investigate.js.map +1 -1
  23. package/dist/tools/cache-analysis.d.ts.map +1 -1
  24. package/dist/tools/cache-analysis.js +438 -21
  25. package/dist/tools/cache-analysis.js.map +1 -1
  26. package/dist/tools/check-health.d.ts.map +1 -1
  27. package/dist/tools/check-health.js +261 -1
  28. package/dist/tools/check-health.js.map +1 -1
  29. package/dist/tools/class-histogram.d.ts.map +1 -1
  30. package/dist/tools/class-histogram.js +142 -33
  31. package/dist/tools/class-histogram.js.map +1 -1
  32. package/dist/tools/closure-inspection.d.ts.map +1 -1
  33. package/dist/tools/closure-inspection.js +238 -25
  34. package/dist/tools/closure-inspection.js.map +1 -1
  35. package/dist/tools/detached-dom.d.ts.map +1 -1
  36. package/dist/tools/detached-dom.js +25 -3
  37. package/dist/tools/detached-dom.js.map +1 -1
  38. package/dist/tools/dev-artifacts.d.ts +33 -0
  39. package/dist/tools/dev-artifacts.d.ts.map +1 -0
  40. package/dist/tools/dev-artifacts.js +165 -0
  41. package/dist/tools/dev-artifacts.js.map +1 -0
  42. package/dist/tools/diff-snapshots.d.ts.map +1 -1
  43. package/dist/tools/diff-snapshots.js +200 -48
  44. package/dist/tools/diff-snapshots.js.map +1 -1
  45. package/dist/tools/duplicated-strings.d.ts.map +1 -1
  46. package/dist/tools/duplicated-strings.js +74 -12
  47. package/dist/tools/duplicated-strings.js.map +1 -1
  48. package/dist/tools/eval.d.ts.map +1 -1
  49. package/dist/tools/eval.js +189 -10
  50. package/dist/tools/eval.js.map +1 -1
  51. package/dist/tools/event-listener-leaks.d.ts.map +1 -1
  52. package/dist/tools/event-listener-leaks.js +235 -13
  53. package/dist/tools/event-listener-leaks.js.map +1 -1
  54. package/dist/tools/event-registry.d.ts +12 -0
  55. package/dist/tools/event-registry.d.ts.map +1 -0
  56. package/dist/tools/event-registry.js +183 -0
  57. package/dist/tools/event-registry.js.map +1 -0
  58. package/dist/tools/find-by-property.d.ts.map +1 -1
  59. package/dist/tools/find-by-property.js +113 -31
  60. package/dist/tools/find-by-property.js.map +1 -1
  61. package/dist/tools/find-by-shape.d.ts +12 -0
  62. package/dist/tools/find-by-shape.d.ts.map +1 -0
  63. package/dist/tools/find-by-shape.js +178 -0
  64. package/dist/tools/find-by-shape.js.map +1 -0
  65. package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
  66. package/dist/tools/find-nodes-by-class.js +50 -6
  67. package/dist/tools/find-nodes-by-class.js.map +1 -1
  68. package/dist/tools/for-each.d.ts.map +1 -1
  69. package/dist/tools/for-each.js +1 -1
  70. package/dist/tools/for-each.js.map +1 -1
  71. package/dist/tools/get-node.d.ts.map +1 -1
  72. package/dist/tools/get-node.js +6 -0
  73. package/dist/tools/get-node.js.map +1 -1
  74. package/dist/tools/get-references.d.ts.map +1 -1
  75. package/dist/tools/get-references.js +35 -6
  76. package/dist/tools/get-references.js.map +1 -1
  77. package/dist/tools/get-referrers.d.ts.map +1 -1
  78. package/dist/tools/get-referrers.js +35 -6
  79. package/dist/tools/get-referrers.js.map +1 -1
  80. package/dist/tools/get-string.d.ts +12 -0
  81. package/dist/tools/get-string.d.ts.map +1 -0
  82. package/dist/tools/get-string.js +223 -0
  83. package/dist/tools/get-string.js.map +1 -0
  84. package/dist/tools/get-value.d.ts +12 -0
  85. package/dist/tools/get-value.d.ts.map +1 -0
  86. package/dist/tools/get-value.js +152 -0
  87. package/dist/tools/get-value.js.map +1 -0
  88. package/dist/tools/global-variables.d.ts.map +1 -1
  89. package/dist/tools/global-variables.js +54 -34
  90. package/dist/tools/global-variables.js.map +1 -1
  91. package/dist/tools/growth-signals.d.ts +12 -0
  92. package/dist/tools/growth-signals.d.ts.map +1 -0
  93. package/dist/tools/growth-signals.js +211 -0
  94. package/dist/tools/growth-signals.js.map +1 -0
  95. package/dist/tools/intern-opportunities.d.ts +12 -0
  96. package/dist/tools/intern-opportunities.d.ts.map +1 -0
  97. package/dist/tools/intern-opportunities.js +870 -0
  98. package/dist/tools/intern-opportunities.js.map +1 -0
  99. package/dist/tools/largest-objects.d.ts.map +1 -1
  100. package/dist/tools/largest-objects.js +37 -14
  101. package/dist/tools/largest-objects.js.map +1 -1
  102. package/dist/tools/load-snapshot.d.ts +70 -0
  103. package/dist/tools/load-snapshot.d.ts.map +1 -1
  104. package/dist/tools/load-snapshot.js +446 -17
  105. package/dist/tools/load-snapshot.js.map +1 -1
  106. package/dist/tools/object-cost-breakdown.d.ts +12 -0
  107. package/dist/tools/object-cost-breakdown.d.ts.map +1 -0
  108. package/dist/tools/object-cost-breakdown.js +439 -0
  109. package/dist/tools/object-cost-breakdown.js.map +1 -0
  110. package/dist/tools/object-shape.d.ts.map +1 -1
  111. package/dist/tools/object-shape.js +109 -2
  112. package/dist/tools/object-shape.js.map +1 -1
  113. package/dist/tools/pinch-points.d.ts.map +1 -1
  114. package/dist/tools/pinch-points.js +9 -9
  115. package/dist/tools/pinch-points.js.map +1 -1
  116. package/dist/tools/property-distribution.d.ts +12 -0
  117. package/dist/tools/property-distribution.d.ts.map +1 -0
  118. package/dist/tools/property-distribution.js +168 -0
  119. package/dist/tools/property-distribution.js.map +1 -0
  120. package/dist/tools/quick-diagnosis.d.ts.map +1 -1
  121. package/dist/tools/quick-diagnosis.js +65 -20
  122. package/dist/tools/quick-diagnosis.js.map +1 -1
  123. package/dist/tools/retainer-summary.d.ts.map +1 -1
  124. package/dist/tools/retainer-summary.js +90 -5
  125. package/dist/tools/retainer-summary.js.map +1 -1
  126. package/dist/tools/retainer-trace.d.ts.map +1 -1
  127. package/dist/tools/retainer-trace.js +173 -43
  128. package/dist/tools/retainer-trace.js.map +1 -1
  129. package/dist/tools/search-nodes.d.ts.map +1 -1
  130. package/dist/tools/search-nodes.js +10 -3
  131. package/dist/tools/search-nodes.js.map +1 -1
  132. package/dist/tools/search-strings.d.ts +12 -0
  133. package/dist/tools/search-strings.d.ts.map +1 -0
  134. package/dist/tools/search-strings.js +182 -0
  135. package/dist/tools/search-strings.js.map +1 -0
  136. package/dist/tools/sequence-analysis.d.ts +12 -0
  137. package/dist/tools/sequence-analysis.d.ts.map +1 -0
  138. package/dist/tools/sequence-analysis.js +244 -0
  139. package/dist/tools/sequence-analysis.js.map +1 -0
  140. package/dist/tools/server-status.d.ts +12 -0
  141. package/dist/tools/server-status.d.ts.map +1 -0
  142. package/dist/tools/server-status.js +41 -0
  143. package/dist/tools/server-status.js.map +1 -0
  144. package/dist/tools/shape-histogram.d.ts.map +1 -1
  145. package/dist/tools/shape-histogram.js +61 -16
  146. package/dist/tools/shape-histogram.js.map +1 -1
  147. package/dist/tools/sliced-strings.d.ts.map +1 -1
  148. package/dist/tools/sliced-strings.js +38 -15
  149. package/dist/tools/sliced-strings.js.map +1 -1
  150. package/dist/tools/snapshot-header.d.ts +28 -0
  151. package/dist/tools/snapshot-header.d.ts.map +1 -0
  152. package/dist/tools/snapshot-header.js +111 -0
  153. package/dist/tools/snapshot-header.js.map +1 -0
  154. package/dist/tools/snapshot-summary.d.ts.map +1 -1
  155. package/dist/tools/snapshot-summary.js +55 -13
  156. package/dist/tools/snapshot-summary.js.map +1 -1
  157. package/dist/tools/snapshots.d.ts +12 -0
  158. package/dist/tools/snapshots.d.ts.map +1 -0
  159. package/dist/tools/snapshots.js +93 -0
  160. package/dist/tools/snapshots.js.map +1 -0
  161. package/dist/tools/stale-collections.d.ts.map +1 -1
  162. package/dist/tools/stale-collections.js +248 -7
  163. package/dist/tools/stale-collections.js.map +1 -1
  164. package/dist/tools/trace-dominators.d.ts.map +1 -1
  165. package/dist/tools/trace-dominators.js +28 -1
  166. package/dist/tools/trace-dominators.js.map +1 -1
  167. package/dist/tools/weakmap-entries.d.ts +12 -0
  168. package/dist/tools/weakmap-entries.d.ts.map +1 -0
  169. package/dist/tools/weakmap-entries.js +156 -0
  170. package/dist/tools/weakmap-entries.js.map +1 -0
  171. package/dist/utils.d.ts +139 -2
  172. package/dist/utils.d.ts.map +1 -1
  173. package/dist/utils.js +448 -35
  174. package/dist/utils.js.map +1 -1
  175. package/package.json +3 -3
package/README.md CHANGED
@@ -6,6 +6,8 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
6
6
 
7
7
  The MCP server loads and analyzes large heap snapshots in memory, which can exceed Node.js's default heap limit. You need to configure `--max-old-space-size=8192` (or higher) to avoid out-of-memory crashes. The configuration examples below include this setting.
8
8
 
9
+ **Snapshot-size ceiling.** The server holds the full parsed graph (nodes, edges, dominator tree, indexes) resident, so its own RSS is roughly **3–5× the on-disk snapshot size**. With the recommended `--max-old-space-size=8192`, snapshots up to ~850 MB on disk have been analyzed reliably; `memlab_load_snapshot` refuses files above `max_file_size_mb` (default 900) to avoid OOM crashes that would lose all state. For larger heaps, raise both `--max-old-space-size` and `max_file_size_mb`. When loading multiple snapshots with `keep_previous: true`, each resident snapshot adds its full graph to RSS — unload ones you're done with via `memlab_snapshots`.
10
+
9
11
  ## Quick Start
10
12
 
11
13
  ### Option 1: Global install
@@ -126,11 +128,89 @@ console.log(`Heap snapshot written to ${snapshot}`);
126
128
 
127
129
  ### `memlab_load_snapshot`
128
130
 
129
- Load and parse a `.heapsnapshot` file. Builds indexes, computes the dominator tree, and calculates retained sizes.
131
+ Load and parse a `.heapsnapshot` file. Builds indexes, computes the dominator tree, and calculates retained sizes. `file_path` may be a local absolute path, a `manifold://bucket/key` URL, or a bare snapshot filename (resolved against the `nest_server_nodejs_heap_snapshots` bucket and fetched via `manifold get`). Pass `keep_previous: true` to keep earlier snapshots resident for diffing/comparison (each gets a handle; manage with `memlab_snapshots`). `quiet` / `suppress_suggestions` set session-wide output controls to trim repeated boilerplate.
132
+
133
+ ```
134
+ Input: { file_path: "snap.heapsnapshot" | "/abs/path" | "manifold://bucket/key",
135
+ alias?: "before", keep_previous?: false, quiet?: false,
136
+ suppress_suggestions?: false, max_file_size_mb?: 900 }
137
+ Output: { status, file_path, node_count, edge_count, total_size, handle }
138
+ ```
139
+
140
+ ### `memlab_snapshot_header`
141
+
142
+ Peek a `.heapsnapshot`'s header (node/edge counts, capture time, file size) **without** loading it — no dominator pass, so it can never wedge or OOM the server the way a full `memlab_load_snapshot` can. Reports whether the capture fits under the current auto-scaled load ceiling, this app's node/edge density, and — when it doesn't fit — the estimated largest same-app capture that *would* fit, so you can pick a loadable snapshot in one step instead of attempting an oversized load and retrying. `file_path` accepts the same three forms as `memlab_load_snapshot`; a Manifold fetch is reused (cached) by a subsequent load.
143
+
144
+ ```
145
+ Input: { file_path: "snap.heapsnapshot" | "/abs/path" | "manifold://bucket/key" }
146
+ Output: file size, node/edge counts, capture date, current ceiling, and a
147
+ loadable ✓ / over-ceiling ✗ verdict with a density-based max-loadable-MB hint
148
+ ```
149
+
150
+ ### `memlab_snapshots`
151
+
152
+ Manage the multi-snapshot session and session output controls.
153
+
154
+ ```
155
+ Input: { action?: "list"|"switch"|"unload", handle?: "before",
156
+ quiet?: bool, suppress_suggestions?: bool }
157
+ Output: resident snapshots (active one marked), or switch/unload result
158
+ ```
159
+
160
+ ### `memlab_property_distribution`
161
+
162
+ For a class/shape and a property, report value cardinality plus the top-K most frequent values. The key tool for diagnosing cardinality explosions (OTel metric attributes, cache keys, per-record fields).
163
+
164
+ ```
165
+ Input: { property: "http.route", class_name?: "Object", shape?: ["a","b"],
166
+ top_k?: 15, min_count?: 1 }
167
+ Output: { scanned, distinct_values, top_values: [{ value, count, pct }] }
168
+ ```
169
+
170
+ ### `memlab_growth_signals`
171
+
172
+ Single-snapshot heuristic that flags likely unbounded growth: Maps/Sets keyed by timestamps or sequential integers, and large ever-growing Arrays. Confirm with a later snapshot + `memlab_diff_snapshots`.
173
+
174
+ ```
175
+ Input: { limit?: 15, min_entries?: 200, min_retained_size?: 262144 }
176
+ Output: candidates with kind, entry count, retained size, sample keys
177
+ ```
178
+
179
+ ### `memlab_sequence_analysis`
180
+
181
+ Trend analysis across an ordered sequence of 3+ snapshots. Loads each transiently (does not change the active snapshot), reports each class's count at every step, and labels "↑ every step" (leak signal) vs "grew net (noisy)". Lists classes new since baseline.
182
+
183
+ ```
184
+ Input: { paths: ["a","b","c"], limit?: 25, min_growth_count?: 50,
185
+ monotonic_only?: false, max_file_size_mb?: 900 }
186
+ Output: per-step heap totals + growing classes with per-step counts and verdict
187
+ ```
188
+
189
+ ### `memlab_dev_artifacts`
190
+
191
+ Browser snapshots: classify large retainers as production vs. dev-only (retained solely via `__REACT_DEVTOOLS_GLOBAL_HOOK__`, `__REDUX_DEVTOOLS_EXTENSION__`, `window.Debug`, …) and total the bytes to exclude from leak headlines. `memlab_detached_dom` also reports the dev-only share inline.
192
+
193
+ ```
194
+ Input: { limit?: 25, min_retained_size?: 524288, only_dev?: false }
195
+ Output: dev-only byte total + per-object classification (production | dev-only via <global>)
196
+ ```
197
+
198
+ ### `memlab_event_registry`
199
+
200
+ Detector for per-model event registries (Backbone/observer): objects mapping event names to arrays of `{callback, context}`. Reports top event names by listener count, listeners-per-host distribution, and a structural-vs-leak verdict.
201
+
202
+ ```
203
+ Input: { min_events?: 2, limit?: 20, timeout_ms?: 45000 }
204
+ Output: registry stats + top events + verdict (structural O(hosts) vs re-subscription leak)
205
+ ```
206
+
207
+ ### `memlab_server_status`
208
+
209
+ Cheap liveness/health check — returns instantly with process RSS, uptime, and resident snapshots. Use to confirm the server is responsive (vs. stuck behind a heavy scan) and to watch RSS against the snapshot-size ceiling.
130
210
 
131
211
  ```
132
- Input: { file_path: "/path/to/snapshot.heapsnapshot" }
133
- Output: { status, file_path, node_count, edge_count, total_size }
212
+ Input: {}
213
+ Output: status, uptime, RSS, resident snapshots
134
214
  ```
135
215
 
136
216
  ### `memlab_snapshot_summary`
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ /**
11
+ * Wall-clock guardrail for heap analysis.
12
+ *
13
+ * Node.js is single-threaded, so a timer cannot interrupt a synchronous
14
+ * full-heap loop mid-iteration — the guardrail has to be COOPERATIVE: hot loops
15
+ * call `tick()` (cheap, clock checked only every few thousand iterations) and a
16
+ * {@link ScanTimeoutError} is thrown once the budget is exceeded. The per-call
17
+ * wrapper (see guardrail.ts) arms a process-global "active" budget around every
18
+ * tool invocation, and the shared `snapshot.nodes`/`edges.forEach` funnels feed
19
+ * it via {@link tickAnalysis}, so any tool that does a full-heap scan is bounded
20
+ * without per-tool wiring.
21
+ *
22
+ * This module imports nothing so it can be shared by utils, heap-state and the
23
+ * guardrail wrapper without creating an import cycle.
24
+ */
25
+ /**
26
+ * Thrown by a budget when a heap walk exceeds its wall-clock limit. Callers
27
+ * either catch it locally to return partial results, or let it bubble to the
28
+ * per-call guardrail which turns it into a clean "analysis stopped" result.
29
+ */
30
+ export declare class ScanTimeoutError extends Error {
31
+ iterations: number;
32
+ timeoutMs: number;
33
+ constructor(iterations: number, timeoutMs: number);
34
+ }
35
+ /**
36
+ * Wall-clock budget. Call `tick()` once per iteration; it cheaply checks
37
+ * elapsed time every few thousand iterations and throws {@link ScanTimeoutError}
38
+ * when the budget is exceeded.
39
+ */
40
+ export declare function makeScanBudget(timeoutMs: number): {
41
+ tick: () => void;
42
+ };
43
+ /**
44
+ * Resolve the analysis timeout in ms: an explicit per-call override wins, then
45
+ * the MEMLAB_ANALYSIS_TIMEOUT_MS env var, then the 90s default. A value <= 0
46
+ * disables the guardrail (returns 0 → never trips).
47
+ */
48
+ export declare function getAnalysisTimeoutMs(override?: number | null): number;
49
+ /** Arm the global budget for one tool call. `timeoutMs <= 0` disables it. */
50
+ export declare function beginAnalysisBudget(timeoutMs: number): void;
51
+ /** Disarm the global budget at the end of a tool call. */
52
+ export declare function endAnalysisBudget(): void;
53
+ /**
54
+ * Cooperative checkpoint for high-iteration loops (e.g. full-heap scans). Cheap
55
+ * no-op when no budget is armed; the clock is only sampled every few thousand
56
+ * calls, so this is meant for loops that run millions of times. For loops with
57
+ * few iterations but expensive bodies (e.g. per-class dominator aggregation),
58
+ * use {@link checkAnalysisDeadline} instead.
59
+ */
60
+ export declare function tickAnalysis(): void;
61
+ /**
62
+ * Unconditional deadline check (samples the clock every call). Use in loops
63
+ * with a small iteration count but an expensive body, where the sampled
64
+ * {@link tickAnalysis} would not check often enough to be effective.
65
+ */
66
+ export declare function checkAnalysisDeadline(): void;
67
+ /** Wall-clock ms elapsed in the current tool call (0 if none armed). */
68
+ export declare function activeElapsedMs(): number;
69
+ /** The timeout the active budget was armed with (0 if disabled/none). */
70
+ export declare function activeTimeoutMs_(): number;
71
+ //# sourceMappingURL=analysis-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-budget.d.ts","sourceRoot":"","sources":["../src/analysis-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,UAAU,EAAE,MAAM;IAClB,SAAS,EAAE,MAAM;gBADjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM;CAK3B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAC,CAWpE;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CASrE;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ /**
11
+ * Wall-clock guardrail for heap analysis.
12
+ *
13
+ * Node.js is single-threaded, so a timer cannot interrupt a synchronous
14
+ * full-heap loop mid-iteration — the guardrail has to be COOPERATIVE: hot loops
15
+ * call `tick()` (cheap, clock checked only every few thousand iterations) and a
16
+ * {@link ScanTimeoutError} is thrown once the budget is exceeded. The per-call
17
+ * wrapper (see guardrail.ts) arms a process-global "active" budget around every
18
+ * tool invocation, and the shared `snapshot.nodes`/`edges.forEach` funnels feed
19
+ * it via {@link tickAnalysis}, so any tool that does a full-heap scan is bounded
20
+ * without per-tool wiring.
21
+ *
22
+ * This module imports nothing so it can be shared by utils, heap-state and the
23
+ * guardrail wrapper without creating an import cycle.
24
+ */
25
+ /**
26
+ * Thrown by a budget when a heap walk exceeds its wall-clock limit. Callers
27
+ * either catch it locally to return partial results, or let it bubble to the
28
+ * per-call guardrail which turns it into a clean "analysis stopped" result.
29
+ */
30
+ export class ScanTimeoutError extends Error {
31
+ iterations;
32
+ timeoutMs;
33
+ constructor(iterations, timeoutMs) {
34
+ super(`Scan exceeded its ${timeoutMs}ms budget after ~${iterations} nodes`);
35
+ this.iterations = iterations;
36
+ this.timeoutMs = timeoutMs;
37
+ this.name = 'ScanTimeoutError';
38
+ }
39
+ }
40
+ /**
41
+ * Wall-clock budget. Call `tick()` once per iteration; it cheaply checks
42
+ * elapsed time every few thousand iterations and throws {@link ScanTimeoutError}
43
+ * when the budget is exceeded.
44
+ */
45
+ export function makeScanBudget(timeoutMs) {
46
+ const start = Date.now();
47
+ let i = 0;
48
+ return {
49
+ tick() {
50
+ i++;
51
+ if ((i & 0x3fff) === 0 && Date.now() - start > timeoutMs) {
52
+ throw new ScanTimeoutError(i, timeoutMs);
53
+ }
54
+ },
55
+ };
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Process-global "active" budget for the current tool call.
59
+ // ---------------------------------------------------------------------------
60
+ const DEFAULT_ANALYSIS_TIMEOUT_MS = 90_000;
61
+ let activeBudget = null;
62
+ let activeStart = 0;
63
+ let activeTimeoutMs = 0;
64
+ /**
65
+ * Resolve the analysis timeout in ms: an explicit per-call override wins, then
66
+ * the MEMLAB_ANALYSIS_TIMEOUT_MS env var, then the 90s default. A value <= 0
67
+ * disables the guardrail (returns 0 → never trips).
68
+ */
69
+ export function getAnalysisTimeoutMs(override) {
70
+ if (override != null && Number.isFinite(override)) {
71
+ return override > 0 ? override : 0;
72
+ }
73
+ const env = Number(process.env.MEMLAB_ANALYSIS_TIMEOUT_MS);
74
+ if (Number.isFinite(env)) {
75
+ return env > 0 ? env : 0;
76
+ }
77
+ return DEFAULT_ANALYSIS_TIMEOUT_MS;
78
+ }
79
+ /** Arm the global budget for one tool call. `timeoutMs <= 0` disables it. */
80
+ export function beginAnalysisBudget(timeoutMs) {
81
+ activeStart = Date.now();
82
+ activeTimeoutMs = timeoutMs;
83
+ activeBudget = timeoutMs > 0 ? makeScanBudget(timeoutMs) : null;
84
+ }
85
+ /** Disarm the global budget at the end of a tool call. */
86
+ export function endAnalysisBudget() {
87
+ activeBudget = null;
88
+ activeTimeoutMs = 0;
89
+ }
90
+ /**
91
+ * Cooperative checkpoint for high-iteration loops (e.g. full-heap scans). Cheap
92
+ * no-op when no budget is armed; the clock is only sampled every few thousand
93
+ * calls, so this is meant for loops that run millions of times. For loops with
94
+ * few iterations but expensive bodies (e.g. per-class dominator aggregation),
95
+ * use {@link checkAnalysisDeadline} instead.
96
+ */
97
+ export function tickAnalysis() {
98
+ activeBudget?.tick();
99
+ }
100
+ /**
101
+ * Unconditional deadline check (samples the clock every call). Use in loops
102
+ * with a small iteration count but an expensive body, where the sampled
103
+ * {@link tickAnalysis} would not check often enough to be effective.
104
+ */
105
+ export function checkAnalysisDeadline() {
106
+ if (activeTimeoutMs > 0 && Date.now() - activeStart > activeTimeoutMs) {
107
+ throw new ScanTimeoutError(-1, activeTimeoutMs);
108
+ }
109
+ }
110
+ /** Wall-clock ms elapsed in the current tool call (0 if none armed). */
111
+ export function activeElapsedMs() {
112
+ return activeStart ? Date.now() - activeStart : 0;
113
+ }
114
+ /** The timeout the active budget was armed with (0 if disabled/none). */
115
+ export function activeTimeoutMs_() {
116
+ return activeTimeoutMs;
117
+ }
118
+ //# sourceMappingURL=analysis-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-budget.js","sourceRoot":"","sources":["../src/analysis-budget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ /**
12
+ * Install a process-wide wall-clock guardrail around every tool call.
13
+ *
14
+ * Monkey-patches `server.tool` so each registered handler is wrapped: a budget
15
+ * is armed for the call (per-call `timeout_ms` arg > MEMLAB_ANALYSIS_TIMEOUT_MS
16
+ * env > 90s default), the heap-iteration funnels (see heap-state.ts) feed it,
17
+ * and if the budget trips the handler's loop throws a {@link ScanTimeoutError}
18
+ * which we convert into a clean "analysis stopped" result instead of letting a
19
+ * runaway scan wedge / OOM the server.
20
+ *
21
+ * Must be called BEFORE the tools are registered so they register wrapped.
22
+ */
23
+ export declare function installAnalysisGuardrail(server: McpServer): void;
24
+ //# sourceMappingURL=guardrail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guardrail.d.ts","sourceRoot":"","sources":["../src/guardrail.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAYvE;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2ChE"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ import { beginAnalysisBudget, endAnalysisBudget, getAnalysisTimeoutMs, activeElapsedMs, ScanTimeoutError, } from './analysis-budget.js';
11
+ import { toolResult } from './utils.js';
12
+ /**
13
+ * Install a process-wide wall-clock guardrail around every tool call.
14
+ *
15
+ * Monkey-patches `server.tool` so each registered handler is wrapped: a budget
16
+ * is armed for the call (per-call `timeout_ms` arg > MEMLAB_ANALYSIS_TIMEOUT_MS
17
+ * env > 90s default), the heap-iteration funnels (see heap-state.ts) feed it,
18
+ * and if the budget trips the handler's loop throws a {@link ScanTimeoutError}
19
+ * which we convert into a clean "analysis stopped" result instead of letting a
20
+ * runaway scan wedge / OOM the server.
21
+ *
22
+ * Must be called BEFORE the tools are registered so they register wrapped.
23
+ */
24
+ export function installAnalysisGuardrail(server) {
25
+ const origTool = server.tool.bind(server);
26
+ server.tool = (...toolArgs) => {
27
+ const name = String(toolArgs[0]);
28
+ const lastIdx = toolArgs.length - 1;
29
+ const handler = toolArgs[lastIdx];
30
+ if (typeof handler === 'function') {
31
+ const inner = handler;
32
+ toolArgs[lastIdx] = async (...hArgs) => {
33
+ // The SDK calls handler(args, extra) for schema tools, handler(extra)
34
+ // otherwise. Read an optional per-call timeout_ms off the params.
35
+ const params = hArgs[0];
36
+ const override = params && typeof params === 'object' && 'timeout_ms' in params
37
+ ? params.timeout_ms
38
+ : undefined;
39
+ const timeoutMs = getAnalysisTimeoutMs(typeof override === 'number' ? override : undefined);
40
+ beginAnalysisBudget(timeoutMs);
41
+ try {
42
+ return await inner(...hArgs);
43
+ }
44
+ catch (e) {
45
+ if (e instanceof ScanTimeoutError) {
46
+ const ran = Math.round(activeElapsedMs() / 1000);
47
+ const limit = Math.round(timeoutMs / 1000);
48
+ return toolResult(`⚠ Analysis stopped: \`${name}\` exceeded the ${limit}s guardrail (ran ~${ran}s) and was halted to keep the MCP server responsive. No complete result was produced.\n\n` +
49
+ `This usually means the heap is very large. Options:\n` +
50
+ `- Narrow the query with filters (min_count / min_self_size / min_retained_size / a specific name or type).\n` +
51
+ `- For memlab_class_histogram, pass include_retained_size:false for an instant count + self-size histogram.\n` +
52
+ `- Prefer targeted node-id tools (memlab_object_shape, memlab_trace_dominators, memlab_retainer_trace) or memlab_auto_investigate over whole-heap scans.\n` +
53
+ `- Raise the limit for this call where supported (timeout_ms) or globally via the MEMLAB_ANALYSIS_TIMEOUT_MS env var (current default ${limit}s; set 0 to disable).`);
54
+ }
55
+ throw e;
56
+ }
57
+ finally {
58
+ endAnalysisBudget();
59
+ }
60
+ };
61
+ }
62
+ return origTool(...toolArgs);
63
+ };
64
+ }
65
+ //# sourceMappingURL=guardrail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guardrail.js","sourceRoot":"","sources":["../src/guardrail.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAItC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,MAAM,QAAQ,GAAI,MAAM,CAAC,IAAc,CAAC,IAAI,CAAC,MAAM,CAAU,CAAC;IAC7D,MAAmC,CAAC,IAAI,GAAG,CAAC,GAAG,QAAmB,EAAE,EAAE;QACrE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,OAAgB,CAAC;YAC/B,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,EAAE,GAAG,KAAgB,EAAE,EAAE;gBAChD,sEAAsE;gBACtE,kEAAkE;gBAClE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACxB,MAAM,QAAQ,GACZ,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,YAAY,IAAI,MAAM;oBAC5D,CAAC,CAAE,MAAiC,CAAC,UAAU;oBAC/C,CAAC,CAAC,SAAS,CAAC;gBAChB,MAAM,SAAS,GAAG,oBAAoB,CACpC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CACpD,CAAC;gBACF,mBAAmB,CAAC,SAAS,CAAC,CAAC;gBAC/B,IAAI,CAAC;oBACH,OAAO,MAAM,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;gBAC/B,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAI,CAAC,YAAY,gBAAgB,EAAE,CAAC;wBAClC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,CAAC;wBACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;wBAC3C,OAAO,UAAU,CACf,yBAAyB,IAAI,mBAAmB,KAAK,qBAAqB,GAAG,2FAA2F;4BACtK,uDAAuD;4BACvD,8GAA8G;4BAC9G,8GAA8G;4BAC9G,2JAA2J;4BAC3J,wIAAwI,KAAK,uBAAuB,CACvK,CAAC;oBACJ,CAAC;oBACD,MAAM,CAAC,CAAC;gBACV,CAAC;wBAAS,CAAC;oBACT,iBAAiB,EAAE,CAAC;gBACtB,CAAC;YACH,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC/B,CAAC,CAAC;AACJ,CAAC"}
@@ -10,6 +10,12 @@
10
10
  import type { IHeapSnapshot } from '@memlab/core';
11
11
  export type SnapshotEnv = 'browser' | 'node' | 'unknown';
12
12
  export interface SnapshotMetadata {
13
+ /**
14
+ * Stable, human-friendly handle for this snapshot within the session.
15
+ * Node ids are only valid relative to the snapshot they were read from;
16
+ * the handle identifies which loaded snapshot a given id belongs to.
17
+ */
18
+ handle: string;
13
19
  filePath: string;
14
20
  fileName: string;
15
21
  nodeCount: number;
@@ -17,9 +23,55 @@ export interface SnapshotMetadata {
17
23
  totalSize: number;
18
24
  env: SnapshotEnv;
19
25
  }
26
+ /**
27
+ * Session-level output controls to trim repeated boilerplate tokens.
28
+ * - `quietHeader`: when true, the per-call snapshot header is printed only
29
+ * once per loaded snapshot instead of on every tool result.
30
+ * - `suppressSuggestions`: when true, tools omit their "Suggested next steps"
31
+ * trailers.
32
+ */
33
+ export interface SessionConfig {
34
+ quietHeader: boolean;
35
+ suppressSuggestions: boolean;
36
+ }
37
+ export declare function getSessionConfig(): SessionConfig;
38
+ export declare function setSessionConfig(patch: Partial<SessionConfig>): SessionConfig;
39
+ /**
40
+ * Returns true exactly once per loaded snapshot when `quietHeader` is on,
41
+ * and always when it is off. Tools call this from `toolResult` to decide
42
+ * whether to prepend the snapshot header.
43
+ */
44
+ export declare function shouldEmitHeader(): boolean;
20
45
  export declare function getSnapshot(): IHeapSnapshot;
21
46
  export declare function getFilePath(): string | null;
22
47
  export declare function getSnapshotEnv(): SnapshotEnv;
23
48
  export declare function getSnapshotMetadata(): SnapshotMetadata | null;
24
- export declare function setSnapshot(snapshot: IHeapSnapshot, filePath: string, metadata: Omit<SnapshotMetadata, 'filePath'>): void;
49
+ /**
50
+ * Register a snapshot and make it the current one.
51
+ *
52
+ * @param snapshot parsed heap snapshot
53
+ * @param filePath absolute path the snapshot was loaded from
54
+ * @param metadata derived stats (handle is assigned here if not supplied)
55
+ * @param opts.alias preferred handle; falls back to the file name
56
+ * @param opts.replace when true (default), unload all other snapshots first,
57
+ * preserving the single-resident memory profile. When false, keep
58
+ * previously-loaded snapshots resident for diffing/comparison.
59
+ */
60
+ export declare function setSnapshot(snapshot: IHeapSnapshot, filePath: string, metadata: Omit<SnapshotMetadata, 'filePath' | 'handle'>, opts?: {
61
+ alias?: string;
62
+ replace?: boolean;
63
+ }): SnapshotMetadata;
64
+ export declare function listSnapshots(): SnapshotMetadata[];
65
+ /**
66
+ * Drop all resident snapshots and reset the current handle. Used to release the
67
+ * previous snapshot's memory BEFORE parsing a replacement, so peak RSS during a
68
+ * large load is one heap rather than two (avoids the GC death-spiral described
69
+ * in P2403258184 §3).
70
+ */
71
+ export declare function clearAllSnapshots(): void;
72
+ export declare function getCurrentHandle(): string | null;
73
+ export declare function getSnapshotByHandle(handle: string): IHeapSnapshot | null;
74
+ export declare function getMetadataByHandle(handle: string): SnapshotMetadata | null;
75
+ export declare function setCurrentSnapshot(handle: string): boolean;
76
+ export declare function removeSnapshot(handle: string): boolean;
25
77
  //# sourceMappingURL=heap-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"heap-state.d.ts","sourceRoot":"","sources":["../src/heap-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAEhD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,WAAW,CAAC;CAClB;AAKD,wBAAgB,WAAW,IAAI,aAAa,CAK3C;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAE7D;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAC3C,IAAI,CAGN"}
1
+ {"version":3,"file":"heap-state.d.ts","sourceRoot":"","sources":["../src/heap-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAGhD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAgCzD,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,WAAW,CAAC;CAClB;AAaD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAWD,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAM7E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAgBD,wBAAgB,WAAW,IAAI,aAAa,CAO3C;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAG3C;AAED,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAG7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,QAAQ,CAAC,EACvD,IAAI,GAAE;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAM,GAC7C,gBAAgB,CAWlB;AAED,wBAAgB,aAAa,IAAI,gBAAgB,EAAE,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAExE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOtD"}
@@ -7,25 +7,168 @@
7
7
  * @format
8
8
  * @oncall memory_lab
9
9
  */
10
- let currentSnapshot = null;
11
- let currentMetadata = null;
10
+ import { tickAnalysis } from './analysis-budget.js';
11
+ // Wrap the snapshot's full-collection iterators so every heap walk feeds the
12
+ // active wall-clock guardrail (see analysis-budget.ts) without each tool having
13
+ // to wire it in. `snapshot.nodes`/`snapshot.edges` are stable objects assigned
14
+ // once at parse time, so patching their `forEach` once is durable; we mark the
15
+ // collection to stay idempotent across the many getSnapshot() calls per session.
16
+ const TICK_PATCHED = '__memlabTickPatched';
17
+ function patchForEachMethods(collection, methods) {
18
+ if (!collection || typeof collection !== 'object')
19
+ return;
20
+ const c = collection;
21
+ if (c[TICK_PATCHED])
22
+ return;
23
+ for (const m of methods) {
24
+ const fn = c[m];
25
+ if (typeof fn !== 'function')
26
+ continue;
27
+ const orig = fn.bind(c);
28
+ c[m] = (cb) => orig((...args) => {
29
+ tickAnalysis();
30
+ return cb(...args);
31
+ });
32
+ }
33
+ Object.defineProperty(c, TICK_PATCHED, { value: true, enumerable: false });
34
+ }
35
+ function instrumentSnapshot(snapshot) {
36
+ const s = snapshot;
37
+ patchForEachMethods(s.nodes, ['forEach', 'forEachTraceable']);
38
+ patchForEachMethods(s.edges, ['forEach']);
39
+ }
40
+ // Multiple snapshots can be resident at once (for diffing and before/after
41
+ // comparison). The "current" handle is the default target for tools that
42
+ // don't take an explicit snapshot argument.
43
+ const loaded = new Map();
44
+ let currentHandle = null;
45
+ const sessionConfig = {
46
+ quietHeader: false,
47
+ suppressSuggestions: false,
48
+ };
49
+ // Tracks whether the header has been emitted since the current snapshot was
50
+ // loaded/activated, so `quietHeader` can print it exactly once.
51
+ let headerEmitted = false;
52
+ export function getSessionConfig() {
53
+ return sessionConfig;
54
+ }
55
+ export function setSessionConfig(patch) {
56
+ if (patch.quietHeader != null)
57
+ sessionConfig.quietHeader = patch.quietHeader;
58
+ if (patch.suppressSuggestions != null) {
59
+ sessionConfig.suppressSuggestions = patch.suppressSuggestions;
60
+ }
61
+ return sessionConfig;
62
+ }
63
+ /**
64
+ * Returns true exactly once per loaded snapshot when `quietHeader` is on,
65
+ * and always when it is off. Tools call this from `toolResult` to decide
66
+ * whether to prepend the snapshot header.
67
+ */
68
+ export function shouldEmitHeader() {
69
+ if (!sessionConfig.quietHeader)
70
+ return true;
71
+ if (headerEmitted)
72
+ return false;
73
+ headerEmitted = true;
74
+ return true;
75
+ }
76
+ function uniqueHandle(base) {
77
+ // Sanitize to a short slug, then disambiguate against existing handles.
78
+ const slug = base
79
+ .replace(/\.heapsnapshot$/i, '')
80
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
81
+ .replace(/^_+|_+$/g, '')
82
+ .slice(0, 40) || 'snapshot';
83
+ if (!loaded.has(slug))
84
+ return slug;
85
+ let i = 2;
86
+ while (loaded.has(`${slug}#${i}`))
87
+ i++;
88
+ return `${slug}#${i}`;
89
+ }
12
90
  export function getSnapshot() {
13
- if (!currentSnapshot) {
91
+ const entry = currentHandle ? loaded.get(currentHandle) : undefined;
92
+ if (!entry) {
14
93
  throw new Error('No heap snapshot loaded. Use memlab_load_snapshot first.');
15
94
  }
16
- return currentSnapshot;
95
+ instrumentSnapshot(entry.snapshot);
96
+ return entry.snapshot;
17
97
  }
18
98
  export function getFilePath() {
19
- return currentMetadata?.filePath ?? null;
99
+ if (!currentHandle)
100
+ return null;
101
+ return loaded.get(currentHandle)?.metadata.filePath ?? null;
20
102
  }
21
103
  export function getSnapshotEnv() {
22
- return currentMetadata?.env ?? 'unknown';
104
+ if (!currentHandle)
105
+ return 'unknown';
106
+ return loaded.get(currentHandle)?.metadata.env ?? 'unknown';
23
107
  }
24
108
  export function getSnapshotMetadata() {
25
- return currentMetadata;
109
+ if (!currentHandle)
110
+ return null;
111
+ return loaded.get(currentHandle)?.metadata ?? null;
112
+ }
113
+ /**
114
+ * Register a snapshot and make it the current one.
115
+ *
116
+ * @param snapshot parsed heap snapshot
117
+ * @param filePath absolute path the snapshot was loaded from
118
+ * @param metadata derived stats (handle is assigned here if not supplied)
119
+ * @param opts.alias preferred handle; falls back to the file name
120
+ * @param opts.replace when true (default), unload all other snapshots first,
121
+ * preserving the single-resident memory profile. When false, keep
122
+ * previously-loaded snapshots resident for diffing/comparison.
123
+ */
124
+ export function setSnapshot(snapshot, filePath, metadata, opts = {}) {
125
+ const replace = opts.replace ?? true;
126
+ if (replace) {
127
+ loaded.clear();
128
+ }
129
+ const handle = uniqueHandle(opts.alias || metadata.fileName);
130
+ const full = { handle, filePath, ...metadata };
131
+ loaded.set(handle, { snapshot, metadata: full });
132
+ currentHandle = handle;
133
+ headerEmitted = false;
134
+ return full;
135
+ }
136
+ export function listSnapshots() {
137
+ return [...loaded.values()].map(l => l.metadata);
138
+ }
139
+ /**
140
+ * Drop all resident snapshots and reset the current handle. Used to release the
141
+ * previous snapshot's memory BEFORE parsing a replacement, so peak RSS during a
142
+ * large load is one heap rather than two (avoids the GC death-spiral described
143
+ * in P2403258184 §3).
144
+ */
145
+ export function clearAllSnapshots() {
146
+ loaded.clear();
147
+ currentHandle = null;
148
+ headerEmitted = false;
149
+ }
150
+ export function getCurrentHandle() {
151
+ return currentHandle;
152
+ }
153
+ export function getSnapshotByHandle(handle) {
154
+ return loaded.get(handle)?.snapshot ?? null;
26
155
  }
27
- export function setSnapshot(snapshot, filePath, metadata) {
28
- currentSnapshot = snapshot;
29
- currentMetadata = { filePath, ...metadata };
156
+ export function getMetadataByHandle(handle) {
157
+ return loaded.get(handle)?.metadata ?? null;
158
+ }
159
+ export function setCurrentSnapshot(handle) {
160
+ if (!loaded.has(handle))
161
+ return false;
162
+ currentHandle = handle;
163
+ headerEmitted = false;
164
+ return true;
165
+ }
166
+ export function removeSnapshot(handle) {
167
+ const existed = loaded.delete(handle);
168
+ if (existed && currentHandle === handle) {
169
+ currentHandle = loaded.size > 0 ? [...loaded.keys()][0] : null;
170
+ headerEmitted = false;
171
+ }
172
+ return existed;
30
173
  }
31
174
  //# sourceMappingURL=heap-state.js.map