@memlab/mcp-server 2.18.1 → 2.86.0

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 (374) hide show
  1. package/README.md +30 -16
  2. package/bin/.eslintrc.js +26 -0
  3. package/bin/memlab-cli.js +313 -0
  4. package/dist/anonymized-snapshot.d.ts +75 -0
  5. package/dist/anonymized-snapshot.d.ts.map +1 -0
  6. package/dist/anonymized-snapshot.js +101 -0
  7. package/dist/anonymized-snapshot.js.map +1 -0
  8. package/dist/artifact-classes.d.ts +86 -0
  9. package/dist/artifact-classes.d.ts.map +1 -0
  10. package/dist/artifact-classes.js +261 -0
  11. package/dist/artifact-classes.js.map +1 -0
  12. package/dist/capture-mode.d.ts +60 -0
  13. package/dist/capture-mode.d.ts.map +1 -0
  14. package/dist/capture-mode.js +39 -0
  15. package/dist/capture-mode.js.map +1 -0
  16. package/dist/eval-hints.d.ts +41 -0
  17. package/dist/eval-hints.d.ts.map +1 -0
  18. package/dist/eval-hints.js +115 -0
  19. package/dist/eval-hints.js.map +1 -0
  20. package/dist/guardrail.d.ts.map +1 -1
  21. package/dist/guardrail.js +13 -0
  22. package/dist/guardrail.js.map +1 -1
  23. package/dist/heap-budget.d.ts +32 -0
  24. package/dist/heap-budget.d.ts.map +1 -0
  25. package/dist/heap-budget.js +90 -0
  26. package/dist/heap-budget.js.map +1 -0
  27. package/dist/heap-shapes.d.ts +148 -0
  28. package/dist/heap-shapes.d.ts.map +1 -0
  29. package/dist/heap-shapes.js +193 -0
  30. package/dist/heap-shapes.js.map +1 -0
  31. package/dist/heap-state.d.ts +109 -2
  32. package/dist/heap-state.d.ts.map +1 -1
  33. package/dist/heap-state.js +194 -3
  34. package/dist/heap-state.js.map +1 -1
  35. package/dist/index.js +102 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/progress.d.ts +25 -0
  38. package/dist/progress.d.ts.map +1 -0
  39. package/dist/progress.js +46 -0
  40. package/dist/progress.js.map +1 -0
  41. package/dist/react-shapes.d.ts +42 -0
  42. package/dist/react-shapes.d.ts.map +1 -0
  43. package/dist/react-shapes.js +149 -0
  44. package/dist/react-shapes.js.map +1 -0
  45. package/dist/result-handles.d.ts +11 -0
  46. package/dist/result-handles.d.ts.map +1 -0
  47. package/dist/result-handles.js +77 -0
  48. package/dist/result-handles.js.map +1 -0
  49. package/dist/server-version.d.ts +19 -0
  50. package/dist/server-version.d.ts.map +1 -0
  51. package/dist/server-version.js +19 -0
  52. package/dist/server-version.js.map +1 -0
  53. package/dist/snapshot-borrow.d.ts +67 -0
  54. package/dist/snapshot-borrow.d.ts.map +1 -0
  55. package/dist/snapshot-borrow.js +113 -0
  56. package/dist/snapshot-borrow.js.map +1 -0
  57. package/dist/string-index.d.ts +55 -0
  58. package/dist/string-index.d.ts.map +1 -0
  59. package/dist/string-index.js +99 -0
  60. package/dist/string-index.js.map +1 -0
  61. package/dist/tool-registry.d.ts +42 -0
  62. package/dist/tool-registry.d.ts.map +1 -0
  63. package/dist/tool-registry.js +20 -0
  64. package/dist/tool-registry.js.map +1 -0
  65. package/dist/tools/analyze-run.d.ts +12 -0
  66. package/dist/tools/analyze-run.d.ts.map +1 -0
  67. package/dist/tools/analyze-run.js +404 -0
  68. package/dist/tools/analyze-run.js.map +1 -0
  69. package/dist/tools/anonymize-snapshot.d.ts +12 -0
  70. package/dist/tools/anonymize-snapshot.d.ts.map +1 -0
  71. package/dist/tools/anonymize-snapshot.js +159 -0
  72. package/dist/tools/anonymize-snapshot.js.map +1 -0
  73. package/dist/tools/app-config.d.ts +12 -0
  74. package/dist/tools/app-config.d.ts.map +1 -0
  75. package/dist/tools/app-config.js +217 -0
  76. package/dist/tools/app-config.js.map +1 -0
  77. package/dist/tools/app-heap.d.ts +30 -0
  78. package/dist/tools/app-heap.d.ts.map +1 -0
  79. package/dist/tools/app-heap.js +106 -0
  80. package/dist/tools/app-heap.js.map +1 -0
  81. package/dist/tools/artifact-budget.d.ts +12 -0
  82. package/dist/tools/artifact-budget.d.ts.map +1 -0
  83. package/dist/tools/artifact-budget.js +219 -0
  84. package/dist/tools/artifact-budget.js.map +1 -0
  85. package/dist/tools/async-census.d.ts +12 -0
  86. package/dist/tools/async-census.d.ts.map +1 -0
  87. package/dist/tools/async-census.js +243 -0
  88. package/dist/tools/async-census.js.map +1 -0
  89. package/dist/tools/auto-investigate.d.ts.map +1 -1
  90. package/dist/tools/auto-investigate.js +54 -5
  91. package/dist/tools/auto-investigate.js.map +1 -1
  92. package/dist/tools/batch.d.ts +12 -0
  93. package/dist/tools/batch.d.ts.map +1 -0
  94. package/dist/tools/batch.js +196 -0
  95. package/dist/tools/batch.js.map +1 -0
  96. package/dist/tools/cache-analysis.d.ts +15 -0
  97. package/dist/tools/cache-analysis.d.ts.map +1 -1
  98. package/dist/tools/cache-analysis.js +240 -14
  99. package/dist/tools/cache-analysis.js.map +1 -1
  100. package/dist/tools/census-diff.d.ts +55 -0
  101. package/dist/tools/census-diff.d.ts.map +1 -0
  102. package/dist/tools/census-diff.js +228 -0
  103. package/dist/tools/census-diff.js.map +1 -0
  104. package/dist/tools/chain-walk.d.ts +12 -0
  105. package/dist/tools/chain-walk.d.ts.map +1 -0
  106. package/dist/tools/chain-walk.js +184 -0
  107. package/dist/tools/chain-walk.js.map +1 -0
  108. package/dist/tools/class-histogram.d.ts.map +1 -1
  109. package/dist/tools/class-histogram.js +14 -3
  110. package/dist/tools/class-histogram.js.map +1 -1
  111. package/dist/tools/collection-diff.d.ts +44 -0
  112. package/dist/tools/collection-diff.d.ts.map +1 -0
  113. package/dist/tools/collection-diff.js +343 -0
  114. package/dist/tools/collection-diff.js.map +1 -0
  115. package/dist/tools/collection-trend.d.ts +55 -0
  116. package/dist/tools/collection-trend.d.ts.map +1 -0
  117. package/dist/tools/collection-trend.js +315 -0
  118. package/dist/tools/collection-trend.js.map +1 -0
  119. package/dist/tools/detached-dom.d.ts +5 -0
  120. package/dist/tools/detached-dom.d.ts.map +1 -1
  121. package/dist/tools/detached-dom.js +368 -22
  122. package/dist/tools/detached-dom.js.map +1 -1
  123. package/dist/tools/dev-artifacts.d.ts +62 -6
  124. package/dist/tools/dev-artifacts.d.ts.map +1 -1
  125. package/dist/tools/dev-artifacts.js +685 -24
  126. package/dist/tools/dev-artifacts.js.map +1 -1
  127. package/dist/tools/diff-snapshots.d.ts.map +1 -1
  128. package/dist/tools/diff-snapshots.js +18 -2
  129. package/dist/tools/diff-snapshots.js.map +1 -1
  130. package/dist/tools/dom-audit.d.ts +37 -0
  131. package/dist/tools/dom-audit.d.ts.map +1 -0
  132. package/dist/tools/dom-audit.js +247 -0
  133. package/dist/tools/dom-audit.js.map +1 -0
  134. package/dist/tools/dominator-attribution.d.ts +12 -0
  135. package/dist/tools/dominator-attribution.d.ts.map +1 -0
  136. package/dist/tools/dominator-attribution.js +189 -0
  137. package/dist/tools/dominator-attribution.js.map +1 -0
  138. package/dist/tools/dominator-chain.d.ts +12 -0
  139. package/dist/tools/dominator-chain.d.ts.map +1 -0
  140. package/dist/tools/dominator-chain.js +92 -0
  141. package/dist/tools/dominator-chain.js.map +1 -0
  142. package/dist/tools/duplicate-objects.d.ts +12 -0
  143. package/dist/tools/duplicate-objects.d.ts.map +1 -0
  144. package/dist/tools/duplicate-objects.js +172 -0
  145. package/dist/tools/duplicate-objects.js.map +1 -0
  146. package/dist/tools/duplicated-strings.d.ts.map +1 -1
  147. package/dist/tools/duplicated-strings.js +49 -40
  148. package/dist/tools/duplicated-strings.js.map +1 -1
  149. package/dist/tools/eval-across.d.ts +12 -0
  150. package/dist/tools/eval-across.d.ts.map +1 -0
  151. package/dist/tools/eval-across.js +135 -0
  152. package/dist/tools/eval-across.js.map +1 -0
  153. package/dist/tools/eval.d.ts +79 -0
  154. package/dist/tools/eval.d.ts.map +1 -1
  155. package/dist/tools/eval.js +2523 -197
  156. package/dist/tools/eval.js.map +1 -1
  157. package/dist/tools/event-listener-leaks.d.ts.map +1 -1
  158. package/dist/tools/event-listener-leaks.js +42 -17
  159. package/dist/tools/event-listener-leaks.js.map +1 -1
  160. package/dist/tools/event-registry.d.ts.map +1 -1
  161. package/dist/tools/event-registry.js +42 -8
  162. package/dist/tools/event-registry.js.map +1 -1
  163. package/dist/tools/explain-delta.d.ts +12 -0
  164. package/dist/tools/explain-delta.d.ts.map +1 -0
  165. package/dist/tools/explain-delta.js +221 -0
  166. package/dist/tools/explain-delta.js.map +1 -0
  167. package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
  168. package/dist/tools/find-nodes-by-class.js +112 -13
  169. package/dist/tools/find-nodes-by-class.js.map +1 -1
  170. package/dist/tools/finding-index.d.ts +203 -0
  171. package/dist/tools/finding-index.d.ts.map +1 -0
  172. package/dist/tools/finding-index.js +566 -0
  173. package/dist/tools/finding-index.js.map +1 -0
  174. package/dist/tools/for-each.d.ts.map +1 -1
  175. package/dist/tools/for-each.js +15 -3
  176. package/dist/tools/for-each.js.map +1 -1
  177. package/dist/tools/get-referrers.d.ts.map +1 -1
  178. package/dist/tools/get-referrers.js +17 -0
  179. package/dist/tools/get-referrers.js.map +1 -1
  180. package/dist/tools/get-value.d.ts +16 -0
  181. package/dist/tools/get-value.d.ts.map +1 -1
  182. package/dist/tools/get-value.js +96 -2
  183. package/dist/tools/get-value.js.map +1 -1
  184. package/dist/tools/hunt-report.d.ts +12 -0
  185. package/dist/tools/hunt-report.d.ts.map +1 -0
  186. package/dist/tools/hunt-report.js +186 -0
  187. package/dist/tools/hunt-report.js.map +1 -0
  188. package/dist/tools/hypothesis.d.ts +12 -0
  189. package/dist/tools/hypothesis.d.ts.map +1 -0
  190. package/dist/tools/hypothesis.js +280 -0
  191. package/dist/tools/hypothesis.js.map +1 -0
  192. package/dist/tools/id-space-audit.d.ts +12 -0
  193. package/dist/tools/id-space-audit.d.ts.map +1 -0
  194. package/dist/tools/id-space-audit.js +262 -0
  195. package/dist/tools/id-space-audit.js.map +1 -0
  196. package/dist/tools/identify.d.ts +12 -0
  197. package/dist/tools/identify.d.ts.map +1 -0
  198. package/dist/tools/identify.js +205 -0
  199. package/dist/tools/identify.js.map +1 -0
  200. package/dist/tools/identity-diff.d.ts +12 -0
  201. package/dist/tools/identity-diff.d.ts.map +1 -0
  202. package/dist/tools/identity-diff.js +188 -0
  203. package/dist/tools/identity-diff.js.map +1 -0
  204. package/dist/tools/intern-opportunities.d.ts.map +1 -1
  205. package/dist/tools/intern-opportunities.js +116 -50
  206. package/dist/tools/intern-opportunities.js.map +1 -1
  207. package/dist/tools/ladder-probe.d.ts +65 -0
  208. package/dist/tools/ladder-probe.d.ts.map +1 -0
  209. package/dist/tools/ladder-probe.js +445 -0
  210. package/dist/tools/ladder-probe.js.map +1 -0
  211. package/dist/tools/ladder.d.ts +33 -0
  212. package/dist/tools/ladder.d.ts.map +1 -0
  213. package/dist/tools/ladder.js +266 -0
  214. package/dist/tools/ladder.js.map +1 -0
  215. package/dist/tools/largest-objects.d.ts.map +1 -1
  216. package/dist/tools/largest-objects.js +6 -2
  217. package/dist/tools/largest-objects.js.map +1 -1
  218. package/dist/tools/leak-report.d.ts +35 -0
  219. package/dist/tools/leak-report.d.ts.map +1 -0
  220. package/dist/tools/leak-report.js +398 -0
  221. package/dist/tools/leak-report.js.map +1 -0
  222. package/dist/tools/load-snapshot.d.ts +29 -0
  223. package/dist/tools/load-snapshot.d.ts.map +1 -1
  224. package/dist/tools/load-snapshot.js +294 -23
  225. package/dist/tools/load-snapshot.js.map +1 -1
  226. package/dist/tools/map-entries.d.ts +12 -0
  227. package/dist/tools/map-entries.d.ts.map +1 -0
  228. package/dist/tools/map-entries.js +139 -0
  229. package/dist/tools/map-entries.js.map +1 -0
  230. package/dist/tools/match-object.d.ts +12 -0
  231. package/dist/tools/match-object.d.ts.map +1 -0
  232. package/dist/tools/match-object.js +165 -0
  233. package/dist/tools/match-object.js.map +1 -0
  234. package/dist/tools/metric.d.ts +13 -0
  235. package/dist/tools/metric.d.ts.map +1 -0
  236. package/dist/tools/metric.js +185 -0
  237. package/dist/tools/metric.js.map +1 -0
  238. package/dist/tools/module-attribution.d.ts +36 -0
  239. package/dist/tools/module-attribution.d.ts.map +1 -0
  240. package/dist/tools/module-attribution.js +235 -0
  241. package/dist/tools/module-attribution.js.map +1 -0
  242. package/dist/tools/next-measurement.d.ts +26 -0
  243. package/dist/tools/next-measurement.d.ts.map +1 -0
  244. package/dist/tools/next-measurement.js +149 -0
  245. package/dist/tools/next-measurement.js.map +1 -0
  246. package/dist/tools/object-cost-breakdown.d.ts.map +1 -1
  247. package/dist/tools/object-cost-breakdown.js +64 -0
  248. package/dist/tools/object-cost-breakdown.js.map +1 -1
  249. package/dist/tools/object-shape.d.ts.map +1 -1
  250. package/dist/tools/object-shape.js +53 -5
  251. package/dist/tools/object-shape.js.map +1 -1
  252. package/dist/tools/population-diff.d.ts +57 -0
  253. package/dist/tools/population-diff.d.ts.map +1 -0
  254. package/dist/tools/population-diff.js +338 -0
  255. package/dist/tools/population-diff.js.map +1 -0
  256. package/dist/tools/population-vs-owners.d.ts +12 -0
  257. package/dist/tools/population-vs-owners.d.ts.map +1 -0
  258. package/dist/tools/population-vs-owners.js +183 -0
  259. package/dist/tools/population-vs-owners.js.map +1 -0
  260. package/dist/tools/property-names.d.ts +29 -0
  261. package/dist/tools/property-names.d.ts.map +1 -0
  262. package/dist/tools/property-names.js +131 -0
  263. package/dist/tools/property-names.js.map +1 -0
  264. package/dist/tools/quick-diagnosis.d.ts.map +1 -1
  265. package/dist/tools/quick-diagnosis.js +7 -1
  266. package/dist/tools/quick-diagnosis.js.map +1 -1
  267. package/dist/tools/react-owners.d.ts +12 -0
  268. package/dist/tools/react-owners.d.ts.map +1 -0
  269. package/dist/tools/react-owners.js +167 -0
  270. package/dist/tools/react-owners.js.map +1 -0
  271. package/dist/tools/react-update-queues.d.ts +12 -0
  272. package/dist/tools/react-update-queues.d.ts.map +1 -0
  273. package/dist/tools/react-update-queues.js +173 -0
  274. package/dist/tools/react-update-queues.js.map +1 -0
  275. package/dist/tools/replicate.d.ts +49 -0
  276. package/dist/tools/replicate.d.ts.map +1 -0
  277. package/dist/tools/replicate.js +241 -0
  278. package/dist/tools/replicate.js.map +1 -0
  279. package/dist/tools/retainer-diff.d.ts +12 -0
  280. package/dist/tools/retainer-diff.d.ts.map +1 -0
  281. package/dist/tools/retainer-diff.js +168 -0
  282. package/dist/tools/retainer-diff.js.map +1 -0
  283. package/dist/tools/retainer-layers.d.ts +23 -0
  284. package/dist/tools/retainer-layers.d.ts.map +1 -0
  285. package/dist/tools/retainer-layers.js +179 -0
  286. package/dist/tools/retainer-layers.js.map +1 -0
  287. package/dist/tools/retainer-summary.d.ts +18 -0
  288. package/dist/tools/retainer-summary.d.ts.map +1 -1
  289. package/dist/tools/retainer-summary.js +210 -51
  290. package/dist/tools/retainer-summary.js.map +1 -1
  291. package/dist/tools/retainer-trace.d.ts.map +1 -1
  292. package/dist/tools/retainer-trace.js +14 -2
  293. package/dist/tools/retainer-trace.js.map +1 -1
  294. package/dist/tools/round-audit.d.ts +66 -0
  295. package/dist/tools/round-audit.d.ts.map +1 -0
  296. package/dist/tools/round-audit.js +258 -0
  297. package/dist/tools/round-audit.js.map +1 -0
  298. package/dist/tools/round-digest.d.ts +12 -0
  299. package/dist/tools/round-digest.d.ts.map +1 -0
  300. package/dist/tools/round-digest.js +190 -0
  301. package/dist/tools/round-digest.js.map +1 -0
  302. package/dist/tools/script-census.d.ts +12 -0
  303. package/dist/tools/script-census.d.ts.map +1 -0
  304. package/dist/tools/script-census.js +182 -0
  305. package/dist/tools/script-census.js.map +1 -0
  306. package/dist/tools/search-nodes.d.ts.map +1 -1
  307. package/dist/tools/search-nodes.js +5 -1
  308. package/dist/tools/search-nodes.js.map +1 -1
  309. package/dist/tools/search-strings.d.ts.map +1 -1
  310. package/dist/tools/search-strings.js +53 -7
  311. package/dist/tools/search-strings.js.map +1 -1
  312. package/dist/tools/sequence-analysis.d.ts +53 -0
  313. package/dist/tools/sequence-analysis.d.ts.map +1 -1
  314. package/dist/tools/sequence-analysis.js +313 -89
  315. package/dist/tools/sequence-analysis.js.map +1 -1
  316. package/dist/tools/server-status.d.ts.map +1 -1
  317. package/dist/tools/server-status.js +4 -1
  318. package/dist/tools/server-status.js.map +1 -1
  319. package/dist/tools/settle-check.d.ts +12 -0
  320. package/dist/tools/settle-check.d.ts.map +1 -0
  321. package/dist/tools/settle-check.js +188 -0
  322. package/dist/tools/settle-check.js.map +1 -0
  323. package/dist/tools/shape-histogram.d.ts.map +1 -1
  324. package/dist/tools/shape-histogram.js +9 -2
  325. package/dist/tools/shape-histogram.js.map +1 -1
  326. package/dist/tools/sliced-strings.d.ts.map +1 -1
  327. package/dist/tools/sliced-strings.js +2 -1
  328. package/dist/tools/sliced-strings.js.map +1 -1
  329. package/dist/tools/snapshots.d.ts.map +1 -1
  330. package/dist/tools/snapshots.js +24 -6
  331. package/dist/tools/snapshots.js.map +1 -1
  332. package/dist/tools/sparse-elements.d.ts +12 -0
  333. package/dist/tools/sparse-elements.d.ts.map +1 -0
  334. package/dist/tools/sparse-elements.js +215 -0
  335. package/dist/tools/sparse-elements.js.map +1 -0
  336. package/dist/tools/stale-collections.d.ts +17 -0
  337. package/dist/tools/stale-collections.d.ts.map +1 -1
  338. package/dist/tools/stale-collections.js +28 -0
  339. package/dist/tools/stale-collections.js.map +1 -1
  340. package/dist/tools/string-patterns.d.ts.map +1 -1
  341. package/dist/tools/string-patterns.js +7 -5
  342. package/dist/tools/string-patterns.js.map +1 -1
  343. package/dist/tools/tools-index.d.ts +12 -0
  344. package/dist/tools/tools-index.d.ts.map +1 -0
  345. package/dist/tools/tools-index.js +492 -0
  346. package/dist/tools/tools-index.js.map +1 -0
  347. package/dist/tools/trace-all.d.ts +42 -0
  348. package/dist/tools/trace-all.d.ts.map +1 -0
  349. package/dist/tools/trace-all.js +263 -0
  350. package/dist/tools/trace-all.js.map +1 -0
  351. package/dist/tools/unit-cost.d.ts +12 -0
  352. package/dist/tools/unit-cost.d.ts.map +1 -0
  353. package/dist/tools/unit-cost.js +219 -0
  354. package/dist/tools/unit-cost.js.map +1 -0
  355. package/dist/tools/verify-fix.d.ts +23 -0
  356. package/dist/tools/verify-fix.d.ts.map +1 -0
  357. package/dist/tools/verify-fix.js +219 -0
  358. package/dist/tools/verify-fix.js.map +1 -0
  359. package/dist/tools/weakmap-entries.d.ts.map +1 -1
  360. package/dist/tools/weakmap-entries.js +6 -1
  361. package/dist/tools/weakmap-entries.js.map +1 -1
  362. package/dist/tools/weakref-census.d.ts +15 -0
  363. package/dist/tools/weakref-census.d.ts.map +1 -0
  364. package/dist/tools/weakref-census.js +147 -0
  365. package/dist/tools/weakref-census.js.map +1 -0
  366. package/dist/tools/what-if.d.ts +12 -0
  367. package/dist/tools/what-if.d.ts.map +1 -0
  368. package/dist/tools/what-if.js +285 -0
  369. package/dist/tools/what-if.js.map +1 -0
  370. package/dist/utils.d.ts +238 -3
  371. package/dist/utils.d.ts.map +1 -1
  372. package/dist/utils.js +507 -15
  373. package/dist/utils.js.map +1 -1
  374. package/package.json +7 -6
package/dist/utils.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall memory_lab
9
9
  */
10
- import { getSnapshotMetadata, getSessionConfig, shouldEmitHeader, } from './heap-state.js';
10
+ import { getSnapshotMetadata, getSessionConfig, isLightSnapshot, shouldEmitHeader, } from './heap-state.js';
11
11
  // ScanTimeoutError / makeScanBudget moved to analysis-budget.ts (shared, no
12
12
  // import cycle). Re-exported here so existing `from '../utils.js'` imports in
13
13
  // the tools keep working unchanged.
@@ -59,6 +59,36 @@ export function truncateDomToTag(name) {
59
59
  }
60
60
  return `${detached}<${tag}>`;
61
61
  }
62
+ // Marker left in place of elided C++ template arguments. Distinctive enough to
63
+ // detect when rendering a legend, and short enough to be worth the swap.
64
+ export const BLINK_TEMPLATE_ELISION = '<…>';
65
+ // A Blink/cppgc type name carries its whole template instantiation, and one hop
66
+ // of a retainer trace can be 400+ tokens of
67
+ // `blink::HeapHashTableBacking<blink::HashTable<cppgc::internal::BasicMember<…>>>`.
68
+ // The information a reader acts on is the OUTER type; the arguments are noise
69
+ // that crowds out the rest of the trace. Collapse everything from the first `<`
70
+ // to the matching final `>` into one marker, keeping any suffix after it.
71
+ //
72
+ // Deliberately conservative: only applied to `::`-qualified C++ names (so a JS
73
+ // class that happens to contain `<` is untouched), and only when the name is
74
+ // long enough for the elision to be worth it — abbreviating
75
+ // `blink::Foo<Bar>` into something barely shorter just costs information.
76
+ const CPP_TEMPLATE_MIN_LEN = 48;
77
+ export function abbreviateBlinkTypeName(name) {
78
+ if (name.length < CPP_TEMPLATE_MIN_LEN)
79
+ return name;
80
+ if (!name.includes('::') || !name.includes('<'))
81
+ return name;
82
+ const open = name.indexOf('<');
83
+ const close = name.lastIndexOf('>');
84
+ if (close <= open)
85
+ return name;
86
+ const head = name.slice(0, open);
87
+ const tail = name.slice(close + 1);
88
+ const abbreviated = `${head}${BLINK_TEMPLATE_ELISION}${tail}`;
89
+ // Never let the "abbreviation" be longer than what it replaced.
90
+ return abbreviated.length < name.length ? abbreviated : name;
91
+ }
62
92
  export function truncateNodeName(name, type, selfSize, maxLen = 150, aggressiveDom = false) {
63
93
  if (type === 'string' ||
64
94
  type === 'concatenated string' ||
@@ -83,9 +113,10 @@ export function truncateNodeName(name, type, selfSize, maxLen = 150, aggressiveD
83
113
  }
84
114
  return name;
85
115
  }
86
- if (name.length <= maxLen)
87
- return name;
88
- return `${name.slice(0, maxLen)}…`;
116
+ const abbreviated = abbreviateBlinkTypeName(name);
117
+ if (abbreviated.length <= maxLen)
118
+ return abbreviated;
119
+ return `${abbreviated.slice(0, maxLen)}…`;
89
120
  }
90
121
  export function serializeNodeSummary(node) {
91
122
  return {
@@ -153,11 +184,21 @@ const NODE_NAME_BLOCK_LIST = new Set([
153
184
  '(External strings)',
154
185
  '(Builtins)',
155
186
  ]);
156
- export function isNodeWorthInspecting(node) {
187
+ /**
188
+ * Whether a node is worth showing in a default "interesting objects" listing.
189
+ *
190
+ * `NODE_TYPE_BLOCK_LIST` exists to keep noise out of the DEFAULT view — it is not
191
+ * meant to make those types unreachable. When a caller explicitly asks for one
192
+ * of them (e.g. `largest_objects({node_type: 'array'})`), honour the request:
193
+ * otherwise the filter silently returns zero rows for `array`, `native`, `code`,
194
+ * `synthetic` and `hidden`, which reads as "there are none" on a heap that
195
+ * plainly contains 100k+ of them.
196
+ */
197
+ export function isNodeWorthInspecting(node, opts) {
157
198
  if (node.id <= 3) {
158
199
  return false;
159
200
  }
160
- if (NODE_TYPE_BLOCK_LIST.has(node.type)) {
201
+ if (!opts?.allowBlockedTypes && NODE_TYPE_BLOCK_LIST.has(node.type)) {
161
202
  return false;
162
203
  }
163
204
  if (NODE_NAME_BLOCK_LIST.has(node.name)) {
@@ -228,11 +269,27 @@ export function looksLikeFailurePayload(value) {
228
269
  /"error"\s*:\s*"(?!null)[^"]+/i.test(head));
229
270
  }
230
271
  export function filterLargestObjects(snapshot, filter, limit) {
272
+ return filterLargestObjectsCounted(snapshot, filter, limit).nodes;
273
+ }
274
+ /**
275
+ * As `filterLargestObjects`, but also reports how many nodes MATCHED.
276
+ *
277
+ * The population size is not a nicety. A conclusion drawn from the top 5 of a
278
+ * matching population reads identically whether that population was 5 or 23,918,
279
+ * and the difference decides whether the conclusion is sound: one report stated
280
+ * "all 5 sampled instances share the same retainer pattern — likely a single
281
+ * root cause" where a full-population trace found 1,468 distinct paths, the
282
+ * largest covering 2.2%. The sample fraction has to reach the caller, so the
283
+ * walk that already visits every node returns it.
284
+ */
285
+ export function filterLargestObjectsCounted(snapshot, filter, limit) {
231
286
  let result = [];
287
+ let matched = 0;
232
288
  snapshot.nodes.forEach(node => {
233
289
  if (!filter(node)) {
234
290
  return;
235
291
  }
292
+ matched++;
236
293
  const size = node.retainedSize;
237
294
  let i;
238
295
  for (i = result.length - 1; i >= 0; --i) {
@@ -246,7 +303,7 @@ export function filterLargestObjects(snapshot, filter, limit) {
246
303
  }
247
304
  result = result.slice(0, limit);
248
305
  });
249
- return result;
306
+ return { nodes: result, matched };
250
307
  }
251
308
  /**
252
309
  * Collapse heap nodes that are merely different VIEWS of the same dominator
@@ -413,6 +470,159 @@ export function boundedDominatorRetainedSize(ids, snapshot, maxWalk = 500) {
413
470
  }
414
471
  return { retained, exact };
415
472
  }
473
+ /**
474
+ * Collect the object-valued slots of a Map/Set/WeakMap backing store, sorted by
475
+ * their true FixedArray slot index.
476
+ *
477
+ * V8 stores entries in an OrderedHashMap/OrderedHashSet reachable via the
478
+ * `table` (or `backing_store`) edge. Browser (Chromium) snapshots type the
479
+ * key/value slot edges as `internal` with a numeric name (the array index);
480
+ * Node snapshots type them `element`. The header counts and hash-chain links are
481
+ * inline SMIs, which emit NO edge at all — so the slot indices are NOT
482
+ * contiguous, and the GAPS are meaningful (they mark SMI-valued or deleted
483
+ * slots). Returning the slots WITH their indices lets callers pair by index
484
+ * instead of by position, which is what makes the pairing correct when some
485
+ * values are SMIs (see `enumerateMapEntries`).
486
+ */
487
+ export function collectBackingSlots(node) {
488
+ const out = [];
489
+ for (const edge of node.references) {
490
+ const eName = String(edge.name_or_index);
491
+ if (eName !== 'table' && eName !== 'backing_store')
492
+ continue;
493
+ const backing = edge.toNode;
494
+ for (const te of backing.references) {
495
+ // Skip the backing store's hidden-class pointer and any hidden edges; the
496
+ // remaining element/internal edges are the real key/value slots.
497
+ if (te.type === 'hidden' || String(te.name_or_index) === 'map')
498
+ continue;
499
+ const idx = Number(te.name_or_index);
500
+ if (!Number.isFinite(idx))
501
+ continue;
502
+ const target = te.toNode;
503
+ // `the_hole` marks a deleted/empty backing-store slot; oddball roots
504
+ // (id<=3) are not real entries. Do NOT skip `undefined`: an explicit
505
+ // `undefined` value/element is a real entry, and dropping it would make it
506
+ // indistinguishable from an inline-primitive gap (mis-reported as absent).
507
+ if (target.id <= 3 || target.name === 'the_hole') {
508
+ continue;
509
+ }
510
+ out.push({ idx, node: target });
511
+ }
512
+ break;
513
+ }
514
+ out.sort((a, b) => a.idx - b.idx);
515
+ return out;
516
+ }
517
+ /**
518
+ * Enumerate a Map/WeakMap's (key, value) entries.
519
+ *
520
+ * V8 lays each entry out as consecutive slots `[key, value, chain]` (the chain
521
+ * link is an inline SMI, so it emits no edge). We therefore pair a key with the
522
+ * value in the immediately-following slot INDEX (`key.idx + 1`). When no slot
523
+ * exists at `key.idx + 1`, the value was an inline SMI/primitive and we emit
524
+ * `value: null` — instead of mispairing the key with the NEXT entry's key, which
525
+ * is what naive "consecutive object edges = key,value" pairing does whenever a
526
+ * Map has SMI values (a real correctness bug on browser heaps).
527
+ */
528
+ export function enumerateMapEntries(node) {
529
+ const slots = collectBackingSlots(node);
530
+ const entries = [];
531
+ let i = 0;
532
+ while (i < slots.length) {
533
+ const key = slots[i];
534
+ const next = slots[i + 1];
535
+ if (next && next.idx === key.idx + 1) {
536
+ entries.push({ key: key.node, value: next.node });
537
+ i += 2;
538
+ }
539
+ else {
540
+ // Value slot absent at key.idx+1 → the value was an inline SMI/primitive.
541
+ entries.push({ key: key.node, value: null });
542
+ i += 1;
543
+ }
544
+ }
545
+ return entries;
546
+ }
547
+ /** Enumerate a Set's elements (every occupied backing-store slot). */
548
+ export function enumerateSetElements(node) {
549
+ return collectBackingSlots(node).map(s => s.node);
550
+ }
551
+ /**
552
+ * Property names that identify an individual record rather than describe its
553
+ * content — always unique per instance, so INCLUDING them in a content
554
+ * signature defeats duplicate detection (every record looks distinct). Excluded
555
+ * by default from `objectContentSignature`. `id`/`__id`/`__ref` are Relay/GraphQL
556
+ * data-id fields; `key`/`clientMutationId` are common per-instance keys.
557
+ */
558
+ export const IDENTITY_PROPS = new Set([
559
+ 'id',
560
+ '__id',
561
+ '__ref',
562
+ 'key',
563
+ 'clientMutationId',
564
+ ]);
565
+ /**
566
+ * A stable, shallow content signature for an object: the sorted set of property
567
+ * names, each annotated with its scalar value (strings capped) or a marker for
568
+ * object/array-valued properties. Two objects with the same signature are
569
+ * structurally identical at one level — the basis for duplicate-record
570
+ * detection (`memlab_duplicate_objects`) and ad-hoc dedup checks in eval.
571
+ *
572
+ * By default per-instance identity fields (`IDENTITY_PROPS`: id/__id/__ref/…) are
573
+ * EXCLUDED so records that differ only by their data-id still collapse together
574
+ * — pass `ignoreProps` to override the set (e.g. `new Set()` to include them).
575
+ *
576
+ * String, boolean, null and object-valued properties are captured (objects
577
+ * generically as `o`, so nested content is not compared).
578
+ *
579
+ * Caveat — numeric values are NOT captured: the V8 heap-snapshot format does not
580
+ * store the actual value of a number field (SMI nodes are `smi number`,
581
+ * heap-numbers are `heap number` — the IEEE-754/int value is not accessible; see
582
+ * `tools/get-value.ts`). A numeric property therefore contributes only a generic
583
+ * `<name>=n` marker, so two objects differing ONLY in a numeric field hash to the
584
+ * same signature and are reported as duplicates. Treat numeric-heavy records
585
+ * accordingly (or compare their values via another tool).
586
+ */
587
+ export function objectContentSignature(node, opts = {}) {
588
+ const maxLen = opts.maxStringLen ?? 40;
589
+ const ignore = opts.ignoreProps ?? IDENTITY_PROPS;
590
+ const parts = [];
591
+ for (const edge of node.references) {
592
+ // Hidden-class ("map") and other internal edges are already excluded by the
593
+ // `edge.type !== 'property'` guard above, so no explicit `map` name skip is
594
+ // needed (it would only hide a legitimate user property literally named "map").
595
+ if (edge.type !== 'property')
596
+ continue;
597
+ const name = String(edge.name_or_index);
598
+ if (name === '__proto__')
599
+ continue;
600
+ if (ignore.has(name))
601
+ continue;
602
+ const t = edge.toNode;
603
+ if (t.isString) {
604
+ const v = t.toStringNode()?.stringValue ?? '';
605
+ parts.push(`${name}=s:${v.length > maxLen ? v.slice(0, maxLen) : v}`);
606
+ }
607
+ else if (t.type === 'number' || t.name === 'heap number') {
608
+ // Numeric values are NOT recoverable from the snapshot format (SMI /
609
+ // heap-number nodes carry no value — see get-value.ts), so we can only
610
+ // record that the property is a number, not distinguish by value.
611
+ parts.push(`${name}=n`);
612
+ }
613
+ else if (t.name === 'true' ||
614
+ t.name === 'false' ||
615
+ t.name === 'null' ||
616
+ t.name === 'undefined') {
617
+ parts.push(`${name}=${t.name}`);
618
+ }
619
+ else {
620
+ parts.push(`${name}=o`);
621
+ }
622
+ }
623
+ parts.sort();
624
+ return parts.join('|');
625
+ }
416
626
  /**
417
627
  * Best-effort detection of which app a Node snapshot came from, by tallying the
418
628
  * `/app(s)/<name>/` segment in bundle paths embedded throughout the heap (string
@@ -489,6 +699,97 @@ export function instrumentationRetainerNote(steps) {
489
699
  }
490
700
  return null;
491
701
  }
702
+ /** Describe a non-property edge in the terms a reader can act on. */
703
+ function describeOwnerRelation(edge) {
704
+ const name = String(edge.name_or_index);
705
+ if (edge.type === 'property' && !/^\d+$/.test(name))
706
+ return null;
707
+ // A captured variable keeps its source name even after minification, which
708
+ // makes it more useful than the anchor it hangs under.
709
+ if (edge.type === 'context')
710
+ return `closure var ${name}`;
711
+ if (/^\d+$/.test(name))
712
+ return 'entry';
713
+ return name;
714
+ }
715
+ export function nearestNamedOwner(node, opts) {
716
+ const maxHops = opts?.maxHops ?? 6;
717
+ let relation = null;
718
+ let current = node;
719
+ for (let hop = 0; hop < maxHops; hop++) {
720
+ // A named property wins over any other referrer at the same hop: it is the
721
+ // only edge that names the object rather than merely containing it.
722
+ let chosen = null;
723
+ for (const edge of current.referrers) {
724
+ const name = String(edge.name_or_index);
725
+ if (edge.type === 'property' && !/^\d+$/.test(name)) {
726
+ chosen = edge;
727
+ break;
728
+ }
729
+ if (!chosen)
730
+ chosen = edge;
731
+ }
732
+ if (!chosen)
733
+ return null;
734
+ // Only the FIRST hop describes how the target itself is held; hops above it
735
+ // are the anchor's own retention and not the caller's concern.
736
+ if (hop === 0)
737
+ relation = describeOwnerRelation(chosen);
738
+ const name = String(chosen.name_or_index);
739
+ if (chosen.type === 'property' && !/^\d+$/.test(name)) {
740
+ const anchor = `${chosen.fromNode.name}.${name}`;
741
+ return {
742
+ anchor,
743
+ relation,
744
+ label: relation == null ? anchor : `${anchor} ▸ ${relation}`,
745
+ ownerNode: chosen.fromNode,
746
+ hops: hop + 1,
747
+ };
748
+ }
749
+ current = chosen.fromNode;
750
+ }
751
+ return null;
752
+ }
753
+ /**
754
+ * V8 emits a WeakMap entry as one synthetic edge on the backing table:
755
+ *
756
+ * `26 / part of key (Foo @644221) -> value (Bar @644219) pair in WeakMap (table @3212981)`
757
+ *
758
+ * A retainer path that ENDS on such an edge proves nothing. The WeakMap holds
759
+ * the value only for as long as something else holds the KEY, and V8 collects
760
+ * value→key cycles, so "retained by a WeakMap" is not a reason an object is
761
+ * alive — the reason is whatever retains the key. Acting on the path as printed
762
+ * produces a fix for a leak that is not there.
763
+ */
764
+ const EPHEMERON_EDGE_RE = /part of key \(([^@()]*)@(\d+)\)\s*->\s*value \(([^@()]*)@(\d+)\) pair in WeakMap/;
765
+ /** Parse a WeakMap ephemeron edge name, or `null` if it is an ordinary edge. */
766
+ export function parseEphemeronEdge(edgeName) {
767
+ if (edgeName == null)
768
+ return null;
769
+ const m = EPHEMERON_EDGE_RE.exec(edgeName);
770
+ if (!m)
771
+ return null;
772
+ return {
773
+ keyName: m[1].trim(),
774
+ keyId: Number(m[2]),
775
+ valueName: m[3].trim(),
776
+ valueId: Number(m[4]),
777
+ };
778
+ }
779
+ /**
780
+ * The caveat to print when a path traverses an ephemeron edge. `atTarget` marks
781
+ * the case where it is the LAST hop, which is the one that invalidates the
782
+ * conclusion outright rather than merely complicating it.
783
+ */
784
+ export function ephemeronCaveat(eph, atTarget) {
785
+ return (`⚠ **This path crosses a WeakMap key→value pair${atTarget ? ' as its FINAL hop' : ''}, so it does not explain retention.** ` +
786
+ `A WeakMap holds \`${eph.valueName || 'the value'}\` only while something else holds the KEY ` +
787
+ `\`${eph.keyName || 'key'}\` @${eph.keyId}, and V8 collects value→key cycles — so "retained by a WeakMap" is never the reason an object is alive. ` +
788
+ `Re-trace from the key to find the real retainer: \`memlab_retainer_trace({node_id: ${eph.keyId}})\`.` +
789
+ (atTarget
790
+ ? ' Until you do, treat this object as UNEXPLAINED, not as a confirmed leak.'
791
+ : ''));
792
+ }
492
793
  export function formatBytes(bytes) {
493
794
  if (!Number.isFinite(bytes) || bytes < 0)
494
795
  return 'N/A';
@@ -527,6 +828,89 @@ export function formatNodeInline(id, name, type, selfSize) {
527
828
  const displayName = selfSize != null ? truncateNodeName(name, type, selfSize, 80) : name;
528
829
  return `@${id} ${displayName} (${type})`;
529
830
  }
831
+ /**
832
+ * Collapse runs of consecutive same-class hops in a retainer path into a single
833
+ * step tagged with `repeatCount`.
834
+ *
835
+ * Linked structures (a React hook update queue's `pending → .next → .next → …`,
836
+ * intrusive lists, promise chains) produce paths that are hundreds of identical
837
+ * `Object → Object → Object` hops long. Rendering every hop costs thousands of
838
+ * tokens and tells the reader nothing the count doesn't: the interesting part is
839
+ * the chain's head, its length, and where it attaches. This keeps the first hop
840
+ * of each run (so the edge into the run is preserved) and folds the rest.
841
+ *
842
+ * Only runs of at least `minRun` are collapsed, so ordinary short paths render
843
+ * exactly as before.
844
+ */
845
+ export function collapseRepeatedRuns(steps, minRun = 3) {
846
+ if (steps.length < minRun)
847
+ return steps;
848
+ const key = (s) => `${s.type}::${s.name}`;
849
+ const out = [];
850
+ // The final step is the node the caller asked about. Never fold it into a
851
+ // run, or the trace stops showing the object under investigation and the
852
+ // "← retained object" marker lands on a summarised run instead.
853
+ const foldLimit = steps.length - 1;
854
+ let i = 0;
855
+ while (i < foldLimit) {
856
+ let j = i + 1;
857
+ while (j < foldLimit && key(steps[j]) === key(steps[i]))
858
+ j++;
859
+ const run = j - i;
860
+ if (run >= minRun) {
861
+ // Keep the first hop (carries the edge into the run) and the last hop's
862
+ // identity is the same class, so one step with a count is lossless enough.
863
+ out.push({ ...steps[i], repeatCount: run });
864
+ }
865
+ else {
866
+ for (let k = i; k < j; k++)
867
+ out.push(steps[k]);
868
+ }
869
+ i = j;
870
+ }
871
+ out.push(steps[foldLimit]);
872
+ return out;
873
+ }
874
+ /**
875
+ * Collapse runs of an identical rendered label, for formatters that have already
876
+ * reduced each hop to a string. Mirrors `collapseRepeatedRuns` for chain-style
877
+ * (`A → B → C`) renderers.
878
+ */
879
+ export function collapseRepeatedLabels(labels, minRun = 3) {
880
+ if (labels.length < minRun)
881
+ return labels;
882
+ const out = [];
883
+ // Mirror collapseRepeatedRuns: the last label is the target, keep it intact.
884
+ const foldLimit = labels.length - 1;
885
+ let i = 0;
886
+ while (i < foldLimit) {
887
+ let j = i + 1;
888
+ while (j < foldLimit && labels[j] === labels[i])
889
+ j++;
890
+ const run = j - i;
891
+ out.push(run >= minRun ? `${labels[i]} ×${run}` : labels[i]);
892
+ for (let k = i + 1; k < j && run < minRun; k++)
893
+ out.push(labels[k]);
894
+ i = j;
895
+ }
896
+ out.push(labels[foldLimit]);
897
+ return out;
898
+ }
899
+ /**
900
+ * One-line note describing what a tool's thresholds excluded from its own
901
+ * output. Printing this everywhere a default threshold exists prevents the
902
+ * failure mode where a tool reports "0" or "none found" and the reader takes it
903
+ * as "there is nothing there", when in fact everything interesting was below the
904
+ * cutoff. (A `dev_artifacts` run reporting `0 B` dev-only — because every
905
+ * console-retained string was under the 512 KB default — nearly landed a
906
+ * measurement artifact in a diff as a production leak.)
907
+ */
908
+ export function describeSkipped(examined, skipped, thresholdLabel) {
909
+ if (skipped <= 0) {
910
+ return `_Examined all ${formatNumber(examined)} candidates; no ${thresholdLabel} filter excluded anything._`;
911
+ }
912
+ return `_Examined ${formatNumber(examined)} candidates; **${formatNumber(skipped)} were skipped by ${thresholdLabel}** and are NOT reflected in the rows above. Lower it if you expect something smaller._`;
913
+ }
530
914
  function formatEdgeLabel(name) {
531
915
  const trimmed = name.length > 40 ? `${name.slice(0, 39)}…` : name;
532
916
  if (/^\d+$/.test(trimmed))
@@ -569,7 +953,10 @@ function formatRetainerLadder(steps, showSizes) {
569
953
  ? ` — ${formatBytes(s.retainedSize)}`
570
954
  : '';
571
955
  const nodeStr = formatNodeInline(s.id, s.name, s.type, s.selfSize);
572
- lines.push(`${nodeStr}${size}${retainerMarker(i, steps.length)}`);
956
+ const repeat = s.repeatCount && s.repeatCount > 1
957
+ ? ` ×${s.repeatCount} (chain of identical hops)`
958
+ : '';
959
+ lines.push(`${nodeStr}${repeat}${size}${retainerMarker(i, steps.length)}`);
573
960
  }
574
961
  return lines.join('\n');
575
962
  }
@@ -618,11 +1005,18 @@ export function formatRetainerTree(steps, opts = {}) {
618
1005
  ? ` — ${formatBytes(s.retainedSize)}`
619
1006
  : '';
620
1007
  const nodeStr = formatNodeInline(s.id, s.name, s.type, s.selfSize);
621
- lines.push(`${pad}${connector}${nodeStr}${size}${retainerMarker(i, steps.length)}`);
1008
+ const repeat = s.repeatCount && s.repeatCount > 1
1009
+ ? ` ×${s.repeatCount} (chain of identical hops)`
1010
+ : '';
1011
+ lines.push(`${pad}${connector}${nodeStr}${repeat}${size}${retainerMarker(i, steps.length)}`);
622
1012
  }
623
1013
  return lines.join('\n');
624
1014
  }
625
1015
  export function formatNodeSummaryTable(nodes) {
1016
+ // On a light load there is no dominator pass, so every `retainedSize` reads
1017
+ // 0. Printing "0 B" in a Retained Size column is a wrong answer wearing the
1018
+ // costume of a right one; say the number does not exist instead.
1019
+ const light = isLightSnapshot();
626
1020
  const headers = ['ID', 'Name', 'Type', 'Self Size', 'Retained Size'];
627
1021
  const rightCols = new Set([3, 4]);
628
1022
  const rows = nodes.map(n => [
@@ -630,9 +1024,22 @@ export function formatNodeSummaryTable(nodes) {
630
1024
  n.name,
631
1025
  n.type,
632
1026
  formatBytes(n.self_size),
633
- formatBytes(n.retained_size),
1027
+ light ? 'n/a (light)' : formatBytes(n.retained_size),
634
1028
  ]);
635
- return markdownTable(headers, rows, rightCols);
1029
+ return markdownTable(headers, rows, rightCols) + blinkLegend(rows.flat());
1030
+ }
1031
+ /**
1032
+ * Legend for elided C++ template arguments, emitted only when something in the
1033
+ * rendered output actually carries the marker — an unconditional legend is the
1034
+ * same token tax the abbreviation exists to remove.
1035
+ */
1036
+ export function blinkLegend(rendered) {
1037
+ for (const cell of rendered) {
1038
+ if (typeof cell === 'string' && cell.includes(BLINK_TEMPLATE_ELISION)) {
1039
+ return `\n\n_\`${BLINK_TEMPLATE_ELISION}\` = C++ template arguments elided; the outer type is what identifies the object. Use \`memlab_get_node\` for the full instantiated name._`;
1040
+ }
1041
+ }
1042
+ return '';
636
1043
  }
637
1044
  export function formatQueryNodesResult(result, offset) {
638
1045
  const partial = result.timed_out
@@ -655,6 +1062,31 @@ export function formatQueryNodesResult(result, offset) {
655
1062
  }
656
1063
  return `Total matching nodes: ${formatNumber(result.total_count)}${partial}`;
657
1064
  }
1065
+ /**
1066
+ * Build a name filter from a caller-supplied pattern.
1067
+ *
1068
+ * Interpreted as a case-insensitive regular expression, falling back to a plain
1069
+ * case-insensitive substring test when the pattern is not valid regex — so a
1070
+ * literal like `blink::UndoStep` works without escaping and a real pattern
1071
+ * still works. An empty/absent pattern matches everything.
1072
+ *
1073
+ * Why this exists: answering "is `blink::UndoStep` present?" previously meant
1074
+ * `class_histogram({node_type: "native", limit: 200})` and reading ~4 KB of
1075
+ * table for one row. Filtering at the source is the difference between a
1076
+ * yes/no answer and a page of output.
1077
+ */
1078
+ export function makeNamePatternTest(pattern) {
1079
+ if (pattern == null || pattern.length === 0)
1080
+ return () => true;
1081
+ try {
1082
+ const re = new RegExp(pattern, 'i');
1083
+ return (name) => re.test(name);
1084
+ }
1085
+ catch {
1086
+ const needle = pattern.toLowerCase();
1087
+ return (name) => name.toLowerCase().includes(needle);
1088
+ }
1089
+ }
658
1090
  export function snapshotHeader() {
659
1091
  const meta = getSnapshotMetadata();
660
1092
  if (!meta)
@@ -666,21 +1098,81 @@ export function snapshotHeader() {
666
1098
  : 'Unknown';
667
1099
  return `> Snapshot: ${meta.fileName} (${formatBytes(meta.totalSize)}, ${formatNumber(meta.nodeCount)} nodes, ${envLabel})`;
668
1100
  }
1101
+ /**
1102
+ * The header for a tool that read snapshots given as PATHS.
1103
+ *
1104
+ * `snapshotHeader()` can only ever describe the RESIDENT snapshot, because that
1105
+ * is the only one the session knows about. For a path-taking tool that is the
1106
+ * wrong file by construction whenever the caller has something else loaded —
1107
+ * and these tools restore the caller's active snapshot before they build their
1108
+ * result (see `withSnapshotAt`), so by the time the header is rendered the
1109
+ * snapshot it names is never the one that was measured.
1110
+ *
1111
+ * Measured across a 12-round investigation: every path-based call printed a
1112
+ * header naming a capture from an earlier round. A ladder result labelled with
1113
+ * another round's rung is a cross-round misattribution hazard in exactly the
1114
+ * workflow these tools exist for, so name what was actually read.
1115
+ */
1116
+ export function pathsHeader(labels) {
1117
+ const seen = labels.filter(l => l.length > 0);
1118
+ if (seen.length === 0)
1119
+ return '';
1120
+ if (seen.length === 1)
1121
+ return `> Snapshot: ${seen[0]}`;
1122
+ if (seen.length === 2)
1123
+ return `> Snapshots: ${seen[0]} → ${seen[1]}`;
1124
+ return `> Snapshots: ${seen.length} rungs, ${seen[0]} … ${seen[seen.length - 1]}`;
1125
+ }
669
1126
  export function textResult(text) {
670
1127
  return { content: [{ type: 'text', text }] };
671
1128
  }
672
- export function toolResult(text) {
673
- const header = shouldEmitHeader() ? snapshotHeader() : '';
1129
+ /**
1130
+ * @param headerOverride Pass `pathsHeader([...])` when the tool read snapshots
1131
+ * by PATH rather than operating on the resident one. Passing `null` suppresses
1132
+ * the header entirely. Omitting it keeps the session-header behaviour, which is
1133
+ * correct only for tools that genuinely act on the active snapshot.
1134
+ */
1135
+ export function toolResult(text, headerOverride) {
1136
+ const header = headerOverride !== undefined
1137
+ ? (headerOverride ?? '')
1138
+ : shouldEmitHeader()
1139
+ ? snapshotHeader()
1140
+ : '';
674
1141
  const body = header ? `${header}\n\n${text}` : text;
675
1142
  return { content: [{ type: 'text', text: body }] };
676
1143
  }
1144
+ /**
1145
+ * After this many suggestion trailers in one server process, the rest are
1146
+ * suppressed automatically.
1147
+ *
1148
+ * The trailer is genuinely useful the first time and pure cost thereafter: it
1149
+ * is the same text on every call, and a long investigation makes dozens of
1150
+ * calls. `suppressSuggestions` already existed but has to be set deliberately,
1151
+ * which means it is set — if at all — only after the tokens have been spent.
1152
+ * Auto-suppressing keeps the onboarding value and drops the repetition without
1153
+ * anyone having to notice.
1154
+ */
1155
+ const SUGGESTION_TRAILER_BUDGET = 5;
1156
+ let suggestionTrailersEmitted = 0;
677
1157
  /**
678
1158
  * Whether tools should omit "Suggested next steps" / "How to fix" trailers.
679
1159
  * Honors the session-level `suppressSuggestions` config so callers can trim
680
- * repeated boilerplate tokens across a long investigation.
1160
+ * repeated boilerplate tokens across a long investigation, and self-suppresses
1161
+ * after `SUGGESTION_TRAILER_BUDGET` trailers.
1162
+ *
1163
+ * NOTE: this both reads and ADVANCES the budget, so call it once per decision.
681
1164
  */
682
1165
  export function suggestionsSuppressed() {
683
- return getSessionConfig().suppressSuggestions;
1166
+ if (getSessionConfig().suppressSuggestions)
1167
+ return true;
1168
+ if (suggestionTrailersEmitted >= SUGGESTION_TRAILER_BUDGET)
1169
+ return true;
1170
+ suggestionTrailersEmitted++;
1171
+ return false;
1172
+ }
1173
+ /** Test seam; also reset when the session config is changed explicitly. */
1174
+ export function resetSuggestionBudget() {
1175
+ suggestionTrailersEmitted = 0;
684
1176
  }
685
1177
  export function jsonResult(data) {
686
1178
  return textResult(JSON.stringify(data, null, 2));