{"kind":"Skill","metadata":{"namespace":"community","name":"openclaw-test-heap-leaks","version":"0.1.0"},"spec":{"description":"Investigate OpenClaw pnpm test memory growth, Vitest OOMs, RSS spikes, and heap snapshot deltas.","files":{"SKILL.md":"---\nname: openclaw-test-heap-leaks\ndescription: Investigate OpenClaw pnpm test memory growth, Vitest OOMs, RSS spikes, and heap snapshot deltas.\n---\n\n# OpenClaw Test Heap Leaks\n\nUse this skill for test-memory investigations. Do not guess from RSS alone when heap snapshots are available. Treat snapshot-name deltas as triage evidence, not proof, until retainers or dominators support the call.\n\nFor **runtime fixes** (e.g., closure leaks in long-running services like the gateway), see [Validating runtime fixes](#validating-runtime-fixes-not-test-memory) below — that uses a dedicated harness, not the test-parallel snapshot machinery.\n\n## Workflow\n\n1. Reproduce the failing shape first.\n   - Match the real entrypoint if possible. For Linux CI-style unit failures, start with:\n   - `pnpm canvas:a2ui:bundle \u0026\u0026 OPENCLAW_TEST_MEMORY_TRACE=1 OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap OPENCLAW_TEST_WORKERS=2 OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 pnpm test`\n   - Keep `OPENCLAW_TEST_MEMORY_TRACE=1` enabled so the wrapper prints per-file RSS summaries alongside the snapshots.\n   - If the report is about a specific shard or worker budget, preserve that shape.\n   - Before you analyze snapshots, identify the real lane names from `[test-parallel] start ...` lines or `pnpm test --plan`. Do not assume a single `unit-fast` lane; local plans often split into `unit-fast-batch-*`.\n\n2. Wait for repeated snapshots before concluding anything.\n   - Take at least two intervals from the same lane.\n   - Compare snapshots from the same PID inside the real lane directory such as `.tmp/heapsnap/unit-fast-batch-2/`.\n   - Use `.agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs` to compare either two files directly or the earliest/latest pair per PID in one lane directory.\n   - If the helper suggests transformed-module retention, confirm the top entries in DevTools retainers/dominators before calling it solved.\n\n3. Classify the growth before choosing a fix.\n   - If growth is dominated by Vite/Vitest transformed source strings, `Module`, `system / Context`, bytecode, descriptor arrays, or property maps, treat it as likely retained module graph growth in long-lived workers.\n   - If growth is dominated by app objects, caches, buffers, server handles, timers, mock state, sqlite state, or similar runtime objects, treat it as a likely cleanup or lifecycle leak.\n   - If the names are ambiguous, stop short of a confident label and inspect retainers/dominators in DevTools for the top deltas.\n\n4. Fix the right layer.\n   - For likely retained transformed-module growth in shared workers:\n   - Prefer timing and hotspot-driven scheduling fixes first. Check whether the file is already represented in `test/fixtures/test-timings.unit.json` and whether `scripts/test-update-memory-hotspots.mjs` should refresh the measured hotspot manifest before hand-editing behavior overrides.\n   - Move hotspot files out of the real shared lane by updating `test/fixtures/test-parallel.behavior.json` only when timing-driven peeling is insufficient.\n   - Prefer `singletonIsolated` for files that are safe alone but inflate shared worker heaps.\n   - If the file should already have been peeled out by timings but is absent from `test/fixtures/test-timings.unit.json`, call that out explicitly. Missing timings are a scheduling blind spot.\n   - For real leaks:\n   - Patch the implicated test or runtime cleanup path.\n   - Look for missing `afterEach`/`afterAll`, module-reset gaps, retained global state, unreleased DB handles, or listeners/timers that survive the file.\n\n5. Verify with the most direct proof.\n   - Re-run the targeted lane or file with heap snapshots enabled if the suite still finishes in reasonable time.\n   - If snapshot overhead pushes tests over Vitest timeouts, fall back to the same lane without snapshots and confirm the RSS trend or OOM is reduced.\n   - For wrapper-only changes, at minimum verify the expected lanes start and the snapshot files are written.\n\n## Heuristics\n\n- Do not call everything a leak. In this repo, large `unit-fast` or `unit-fast-batch-*` growth can be a worker-lifetime problem rather than an application object leak.\n- `scripts/test-parallel.mjs` and `scripts/test-parallel-memory.mjs` are the primary control points for wrapper diagnostics.\n- The lane names printed by `[test-parallel] start ...` and `[test-parallel][mem] summary ...` tell you where to focus.\n- When one or two files account for most of the delta and they are missing from timings, reducing impact by isolating them is usually the first pragmatic fix.\n- When the same retained object families grow across multiple intervals in the same worker PID, trust the snapshots over intuition, then confirm ambiguous calls with retainer evidence.\n\n## Snapshot Comparison\n\n- Direct comparison:\n  - `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs before.heapsnapshot after.heapsnapshot`\n- Auto-select earliest/latest snapshots per PID within one lane:\n  - `node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs --lane-dir .tmp/heapsnap/unit-fast-batch-2`\n- Useful flags:\n  - `--top 40`\n  - `--min-kb 32`\n  - `--pid 16133`\n\nRead the top positive deltas first. Large positive growth in module-transform artifacts suggests lane isolation; large positive growth in runtime objects suggests a real leak. If the names alone do not settle it, open the same snapshot pair in DevTools and inspect retainers/dominators for the top rows before declaring root cause.\n\n## Validating runtime fixes (not test-memory)\n\nThe workflow above is for diagnosing Vitest worker memory growth. For\nvalidating that a runtime/closure fix actually releases captured state, use the\ndedicated harness:\n\n- `pnpm leak:embedded-run` — runs `scripts/embedded-run-abort-leak.ts`. Loops N\n  aborted runs in a function-shaped scope mimicking `runEmbeddedAttempt`,\n  writes heap snapshots, and reports a PASS/FAIL verdict on retention growth\n  using `FinalizationRegistry` for tracked-instance counting plus RSS delta.\n\nModes:\n\n- `closure-extracted` (default) — production fix shape (helper at module scope).\n- `closure-inline` — pre-fix shape (closure inside the runner scope). Use as a\n  sensitivity check: if it passes you've broken the harness, not fixed a bug.\n- `synthetic-leak` — deliberately retains via a module-level bucket. Use to\n  confirm the harness can detect leaks before trusting a PASS on a real fix.\n\nSnapshots land in `.tmp/embedded-run-abort-leak/`. Diff with the same script\nas above:\n\n```\nnode .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs \\\n  .tmp/embedded-run-abort-leak/baseline-*.heapsnapshot \\\n  .tmp/embedded-run-abort-leak/batch-N-*.heapsnapshot --top 30\n```\n\nWhen fixing a different runtime leak, add a new harness alongside this one\nrather than retrofitting it. The fixture function should mimic the lexical\nscope of the function where the leak lives, not be a generic abort-loop.\n\n## Output Expectations\n\nWhen using this skill, report:\n\n- The exact reproduce command.\n- Which lane and PID were compared.\n- The dominant retained object families from the snapshot delta.\n- Whether the issue is a likely real leak or likely shared-worker retained module growth, plus whether retainers/dominators confirmed it.\n- The concrete fix or impact-reduction patch.\n- What you verified, and what snapshot overhead prevented you from verifying.\n","agents/openai.yaml":"interface:\n  display_name: \"Test Heap Leaks\"\n  short_description: \"Investigate test OOMs with heap snapshots\"\n  default_prompt: \"Use $openclaw-test-heap-leaks to investigate test memory growth with heap snapshots and reduce its impact.\"\n","scripts/heapsnapshot-delta.mjs":"#!/usr/bin/env node\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nfunction printUsage() {\n  console.error(\n    \"Usage: node heapsnapshot-delta.mjs \u003cbefore.heapsnapshot\u003e \u003cafter.heapsnapshot\u003e [--top N] [--min-kb N]\",\n  );\n  console.error(\n    \"   or: node heapsnapshot-delta.mjs --lane-dir \u003cdir\u003e [--pid PID] [--top N] [--min-kb N]\",\n  );\n}\n\nfunction fail(message) {\n  console.error(message);\n  process.exit(1);\n}\n\nfunction parseArgs(argv) {\n  const options = {\n    top: 30,\n    minKb: 64,\n    laneDir: null,\n    pid: null,\n    files: [],\n  };\n\n  for (let index = 0; index \u003c argv.length; index += 1) {\n    const arg = argv[index];\n    if (arg === \"--top\") {\n      options.top = Number.parseInt(argv[index + 1] ?? \"\", 10);\n      index += 1;\n      continue;\n    }\n    if (arg === \"--min-kb\") {\n      options.minKb = Number.parseInt(argv[index + 1] ?? \"\", 10);\n      index += 1;\n      continue;\n    }\n    if (arg === \"--lane-dir\") {\n      options.laneDir = argv[index + 1] ?? null;\n      index += 1;\n      continue;\n    }\n    if (arg === \"--pid\") {\n      options.pid = Number.parseInt(argv[index + 1] ?? \"\", 10);\n      index += 1;\n      continue;\n    }\n    options.files.push(arg);\n  }\n\n  if (!Number.isFinite(options.top) || options.top \u003c= 0) {\n    fail(\"--top must be a positive integer\");\n  }\n  if (!Number.isFinite(options.minKb) || options.minKb \u003c 0) {\n    fail(\"--min-kb must be a non-negative integer\");\n  }\n  if (options.pid !== null \u0026\u0026 (!Number.isInteger(options.pid) || options.pid \u003c= 0)) {\n    fail(\"--pid must be a positive integer\");\n  }\n\n  return options;\n}\n\nclass JsonStreamScanner {\n  constructor(filePath) {\n    this.stream = fs.createReadStream(filePath, {\n      encoding: \"utf8\",\n      highWaterMark: 1024 * 1024,\n    });\n    this.iterator = this.stream[Symbol.asyncIterator]();\n    this.buffer = \"\";\n    this.offset = 0;\n    this.done = false;\n  }\n\n  compactBuffer() {\n    if (this.offset \u003e 65536) {\n      this.buffer = this.buffer.slice(this.offset);\n      this.offset = 0;\n    }\n  }\n\n  async ensureAvailable(count = 1) {\n    while (!this.done \u0026\u0026 this.buffer.length - this.offset \u003c count) {\n      const next = await this.iterator.next();\n      if (next.done) {\n        this.done = true;\n        break;\n      }\n      this.buffer += next.value;\n    }\n  }\n\n  async peek() {\n    await this.ensureAvailable(1);\n    return this.buffer[this.offset] ?? null;\n  }\n\n  async next() {\n    await this.ensureAvailable(1);\n    if (this.offset \u003e= this.buffer.length) {\n      return null;\n    }\n    const char = this.buffer[this.offset];\n    this.offset += 1;\n    this.compactBuffer();\n    return char;\n  }\n\n  async skipWhitespace() {\n    while (true) {\n      const char = await this.peek();\n      if (char === null || !/\\s/u.test(char)) {\n        return;\n      }\n      await this.next();\n    }\n  }\n\n  async expectChar(expected) {\n    const char = await this.next();\n    if (char !== expected) {\n      fail(`Expected ${expected} but found ${char ?? \"\u003ceof\u003e\"}`);\n    }\n  }\n\n  async find(sequence) {\n    let matched = 0;\n    while (true) {\n      const char = await this.next();\n      if (char === null) {\n        fail(`Could not find ${sequence}`);\n      }\n      if (char === sequence[matched]) {\n        matched += 1;\n        if (matched === sequence.length) {\n          return;\n        }\n        continue;\n      }\n      matched = char === sequence[0] ? 1 : 0;\n      if (matched === sequence.length) {\n        return;\n      }\n    }\n  }\n\n  async readBalancedObject() {\n    const start = await this.next();\n    if (start !== \"{\") {\n      fail(`Expected { but found ${start ?? \"\u003ceof\u003e\"}`);\n    }\n    let text = \"{\";\n    let depth = 1;\n    let inString = false;\n    let escaped = false;\n    while (depth \u003e 0) {\n      const char = await this.next();\n      if (char === null) {\n        fail(\"Unexpected EOF while reading JSON object\");\n      }\n      text += char;\n      if (inString) {\n        if (escaped) {\n          escaped = false;\n        } else if (char === \"\\\\\") {\n          escaped = true;\n        } else if (char === '\"') {\n          inString = false;\n        }\n        continue;\n      }\n      if (char === '\"') {\n        inString = true;\n      } else if (char === \"{\") {\n        depth += 1;\n      } else if (char === \"}\") {\n        depth -= 1;\n      }\n    }\n    return text;\n  }\n\n  async parseNumberArray(onValue) {\n    await this.skipWhitespace();\n    await this.expectChar(\"[\");\n    await this.skipWhitespace();\n    if ((await this.peek()) === \"]\") {\n      await this.next();\n      return;\n    }\n\n    let token = \"\";\n    let index = 0;\n    const flush = () =\u003e {\n      if (token.length === 0) {\n        fail(\"Unexpected empty number token\");\n      }\n      const value = Number.parseInt(token, 10);\n      if (!Number.isFinite(value)) {\n        fail(`Invalid numeric token: ${token}`);\n      }\n      onValue(value, index);\n      index += 1;\n      token = \"\";\n    };\n\n    while (true) {\n      const char = await this.next();\n      if (char === null) {\n        fail(\"Unexpected EOF while reading number array\");\n      }\n      if (char === \"]\") {\n        flush();\n        return;\n      }\n      if (char === \",\") {\n        flush();\n        continue;\n      }\n      if (/\\s/u.test(char)) {\n        continue;\n      }\n      token += char;\n    }\n  }\n\n  async readJsonString() {\n    await this.expectChar('\"');\n    let value = \"\";\n    while (true) {\n      const char = await this.next();\n      if (char === null) {\n        fail(\"Unexpected EOF while reading JSON string\");\n      }\n      if (char === '\"') {\n        return value;\n      }\n      if (char !== \"\\\\\") {\n        value += char;\n        continue;\n      }\n      const escaped = await this.next();\n      if (escaped === null) {\n        fail(\"Unexpected EOF while reading JSON string escape\");\n      }\n      if (escaped === \"u\") {\n        let hex = \"\";\n        for (let index = 0; index \u003c 4; index += 1) {\n          const hexChar = await this.next();\n          if (hexChar === null) {\n            fail(\"Unexpected EOF while reading JSON unicode escape\");\n          }\n          hex += hexChar;\n        }\n        value += String.fromCharCode(Number.parseInt(hex, 16));\n        continue;\n      }\n      value +=\n        escaped === \"b\"\n          ? \"\\b\"\n          : escaped === \"f\"\n            ? \"\\f\"\n            : escaped === \"n\"\n              ? \"\\n\"\n              : escaped === \"r\"\n                ? \"\\r\"\n                : escaped === \"t\"\n                  ? \"\\t\"\n                  : escaped;\n    }\n  }\n\n  async parseStringArray(onValue) {\n    await this.skipWhitespace();\n    await this.expectChar(\"[\");\n    await this.skipWhitespace();\n    if ((await this.peek()) === \"]\") {\n      await this.next();\n      return;\n    }\n\n    let index = 0;\n    while (true) {\n      const value = await this.readJsonString();\n      onValue(value, index);\n      index += 1;\n      await this.skipWhitespace();\n      const separator = await this.next();\n      if (separator === \"]\") {\n        return;\n      }\n      if (separator !== \",\") {\n        fail(`Expected , or ] but found ${separator ?? \"\u003ceof\u003e\"}`);\n      }\n      await this.skipWhitespace();\n    }\n  }\n}\n\nfunction parseHeapFilename(filePath) {\n  const base = path.basename(filePath);\n  const match = base.match(\n    /^Heap\\.(?\u003cstamp\u003e\\d{8}\\.\\d{6})\\.(?\u003cpid\u003e\\d+)\\.0\\.(?\u003cseq\u003e\\d+)\\.heapsnapshot$/u,\n  );\n  if (!match?.groups) {\n    return null;\n  }\n  return {\n    filePath,\n    pid: Number.parseInt(match.groups.pid, 10),\n    stamp: match.groups.stamp,\n    sequence: Number.parseInt(match.groups.seq, 10),\n  };\n}\n\nfunction resolvePair(options) {\n  if (options.laneDir) {\n    const entries = fs\n      .readdirSync(options.laneDir)\n      .map((name) =\u003e parseHeapFilename(path.join(options.laneDir, name)))\n      .filter((entry) =\u003e entry !== null)\n      .filter((entry) =\u003e options.pid === null || entry.pid === options.pid)\n      .toSorted((left, right) =\u003e {\n        if (left.pid !== right.pid) {\n          return left.pid - right.pid;\n        }\n        if (left.stamp !== right.stamp) {\n          return left.stamp.localeCompare(right.stamp);\n        }\n        return left.sequence - right.sequence;\n      });\n\n    if (entries.length === 0) {\n      fail(`No matching heap snapshots found in ${options.laneDir}`);\n    }\n\n    const groups = new Map();\n    for (const entry of entries) {\n      const group = groups.get(entry.pid) ?? [];\n      group.push(entry);\n      groups.set(entry.pid, group);\n    }\n\n    const candidates = Array.from(groups.values())\n      .map((group) =\u003e ({\n        pid: group[0].pid,\n        before: group[0],\n        after: group.at(-1),\n        count: group.length,\n      }))\n      .filter((entry) =\u003e entry.count \u003e= 2);\n\n    if (candidates.length === 0) {\n      fail(`Need at least two snapshots for one PID in ${options.laneDir}`);\n    }\n\n    const chosen =\n      options.pid !== null\n        ? (candidates.find((entry) =\u003e entry.pid === options.pid) ?? null)\n        : candidates.toSorted((left, right) =\u003e right.count - left.count || left.pid - right.pid)[0];\n\n    if (!chosen) {\n      fail(`No PID with at least two snapshots matched in ${options.laneDir}`);\n    }\n\n    return {\n      before: chosen.before.filePath,\n      after: chosen.after.filePath,\n      pid: chosen.pid,\n      snapshotCount: chosen.count,\n    };\n  }\n\n  if (options.files.length !== 2) {\n    printUsage();\n    process.exit(1);\n  }\n\n  return {\n    before: options.files[0],\n    after: options.files[1],\n    pid: null,\n    snapshotCount: 2,\n  };\n}\n\nasync function parseSnapshotMeta(scanner) {\n  await scanner.find('\"snapshot\":');\n  await scanner.skipWhitespace();\n  const metaObjectText = await scanner.readBalancedObject();\n  const parsed = JSON.parse(metaObjectText);\n  return parsed?.meta ?? null;\n}\n\nasync function buildSummary(filePath) {\n  const scanner = new JsonStreamScanner(filePath);\n  const meta = await parseSnapshotMeta(scanner);\n  if (!meta) {\n    fail(`Invalid heap snapshot: ${filePath}`);\n  }\n\n  const nodeFieldCount = meta.node_fields.length;\n  const typeNames = meta.node_types[0];\n  const typeIndex = meta.node_fields.indexOf(\"type\");\n  const nameIndex = meta.node_fields.indexOf(\"name\");\n  const selfSizeIndex = meta.node_fields.indexOf(\"self_size\");\n  if (typeIndex === -1 || nameIndex === -1 || selfSizeIndex === -1) {\n    fail(`Unsupported heap snapshot schema: ${filePath}`);\n  }\n\n  const summaryByIndex = new Map();\n  let nodeCount = 0;\n  let currentTypeId = 0;\n  let currentNameId = 0;\n  let currentSelfSize = 0;\n  await scanner.find('\"nodes\":');\n  await scanner.parseNumberArray((value, index) =\u003e {\n    const fieldIndex = index % nodeFieldCount;\n    if (fieldIndex === typeIndex) {\n      currentTypeId = value;\n      return;\n    }\n    if (fieldIndex === nameIndex) {\n      currentNameId = value;\n      return;\n    }\n    if (fieldIndex === selfSizeIndex) {\n      currentSelfSize = value;\n    }\n    if (fieldIndex !== nodeFieldCount - 1) {\n      return;\n    }\n    const key = `${currentTypeId}\\t${currentNameId}`;\n    const current = summaryByIndex.get(key) ?? {\n      typeId: currentTypeId,\n      nameId: currentNameId,\n      selfSize: 0,\n      count: 0,\n    };\n    current.selfSize += currentSelfSize;\n    current.count += 1;\n    summaryByIndex.set(key, current);\n    nodeCount += 1;\n  });\n\n  const requiredNameIds = new Set(\n    Array.from(summaryByIndex.values(), (entry) =\u003e entry.nameId).filter((value) =\u003e value \u003e= 0),\n  );\n  const nameStrings = new Map();\n  await scanner.find('\"strings\":');\n  await scanner.parseStringArray((value, index) =\u003e {\n    if (requiredNameIds.has(index)) {\n      nameStrings.set(index, value);\n    }\n  });\n\n  const summary = new Map();\n  for (const entry of summaryByIndex.values()) {\n    const key = `${typeNames[entry.typeId] ?? \"unknown\"}\\t${nameStrings.get(entry.nameId) ?? \"\"}`;\n    summary.set(key, {\n      type: typeNames[entry.typeId] ?? \"unknown\",\n      name: nameStrings.get(entry.nameId) ?? \"\",\n      selfSize: entry.selfSize,\n      count: entry.count,\n    });\n  }\n\n  return {\n    nodeCount,\n    summary,\n  };\n}\n\nfunction formatBytes(bytes) {\n  if (Math.abs(bytes) \u003e= 1024 ** 2) {\n    return `${(bytes / 1024 ** 2).toFixed(2)} MiB`;\n  }\n  if (Math.abs(bytes) \u003e= 1024) {\n    return `${(bytes / 1024).toFixed(1)} KiB`;\n  }\n  return `${bytes} B`;\n}\n\nfunction formatDelta(bytes) {\n  return `${bytes \u003e= 0 ? \"+\" : \"-\"}${formatBytes(Math.abs(bytes))}`;\n}\n\nfunction truncate(text, maxLength) {\n  return text.length \u003c= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;\n}\n\nasync function main() {\n  const options = parseArgs(process.argv.slice(2));\n  const pair = resolvePair(options);\n  const before = await buildSummary(pair.before);\n  const after = await buildSummary(pair.after);\n  const minBytes = options.minKb * 1024;\n\n  const rows = [];\n  for (const [key, next] of after.summary) {\n    const previous = before.summary.get(key) ?? { selfSize: 0, count: 0 };\n    const sizeDelta = next.selfSize - previous.selfSize;\n    const countDelta = next.count - previous.count;\n    if (sizeDelta \u003c minBytes) {\n      continue;\n    }\n    rows.push({\n      type: next.type,\n      name: next.name,\n      sizeDelta,\n      countDelta,\n      afterSize: next.selfSize,\n      afterCount: next.count,\n    });\n  }\n\n  rows.sort(\n    (left, right) =\u003e right.sizeDelta - left.sizeDelta || right.countDelta - left.countDelta,\n  );\n\n  console.log(`before: ${pair.before}`);\n  console.log(`after:  ${pair.after}`);\n  if (pair.pid !== null) {\n    console.log(`pid:    ${pair.pid} (${pair.snapshotCount} snapshots found)`);\n  }\n  console.log(\n    `nodes:   ${before.nodeCount} -\u003e ${after.nodeCount} (${after.nodeCount - before.nodeCount \u003e= 0 ? \"+\" : \"\"}${after.nodeCount - before.nodeCount})`,\n  );\n  console.log(`filter:  top=${options.top} min=${options.minKb} KiB`);\n  console.log(\"\");\n\n  if (rows.length === 0) {\n    console.log(\"No entries exceeded the minimum delta.\");\n    return;\n  }\n\n  for (const row of rows.slice(0, options.top)) {\n    console.log(\n      [\n        formatDelta(row.sizeDelta).padStart(11),\n        `count ${row.countDelta \u003e= 0 ? \"+\" : \"\"}${row.countDelta}`.padStart(10),\n        row.type.padEnd(16),\n        truncate(row.name || \"(empty)\", 96),\n      ].join(\"  \"),\n    );\n  }\n}\n\nawait main();\n"},"import":{"commit_sha":"424c6d0a5f4665b803ad6768d08b0be7659deaf4","imported_at":"2026-05-18T20:13:36Z","license_text":"MIT License\n\nCopyright (c) 2025 Peter Steinberger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","owner":"openclaw","repo":"openclaw/openclaw","source_url":"https://github.com/openclaw/openclaw/tree/424c6d0a5f4665b803ad6768d08b0be7659deaf4/.agents/skills/openclaw-test-heap-leaks"}},"content_hash":[170,50,246,223,167,154,231,222,162,169,219,227,60,235,140,244,26,54,77,137,162,19,251,250,147,80,56,27,187,208,49,8],"trust_level":"unsigned","yanked":false}
