# @lensmcp/memory-tracker

Track memory growth in long-lived JavaScript containers, attribute it to the flow that caused it, and surface suspected leaks as LensMCP events.

`@lensmcp/memory-tracker` instruments the mutating methods of `Map`, `Set`, and `Array` instances in place. Every mutation updates a per-container "owner" record (item count plus a rough byte estimate) and is attributed to the active request/flow. When a flow grows a container past a threshold and the items do not shrink back, the tracker emits a `memory-retention` ("memory leak suspected") event. It also reports stale generations of disposed singletons that still retain bytes. The result is a deterministic, attributable memory signal in the LensMCP lens — instead of a flat heap graph, you see *which owner grew, by how much, and under which flow*.

It is designed to run in "light mode" inside the LensMCP node/NestJS instrumentation: zero sampling timers in the hot path, bounded per-mutation work, and a pluggable event sink so the host process forwards events to the LensMCP bus.

## Install

```bash
yarn add @lensmcp/memory-tracker
```

Most users do not install this directly. It is normally enabled through the `memory` option of `@lensmcp/nest-instrumentation` (or the node instrumentation), which calls `configureMemoryTracker` and wires the flow brackets for you. Reach for the direct API only when instrumenting a runtime that LensMCP does not auto-wire.

## Usage

Configure the tracker once at startup, track the containers you care about, then bracket each unit of work so growth can be attributed and leaks detected.

```ts
import {
  configureMemoryTracker,
  trackContainer,
  onFlowStart,
  onFlowSettled,
} from '@lensmcp/memory-tracker';

// 1. Configure once. `sink` receives every memory BaseEvent; the host
//    forwards it to the LensMCP bus. `contextGetter` lets each mutation
//    attribute itself to the active flow (e.g. an AsyncLocalStorage store).
configureMemoryTracker({
  sessionId: 'session-123',
  sink: (event) => bus.publish(event),
  leakItemThreshold: 50,                       // items of growth that flips a suspected leak (default 50)
  staleGenerationBytesThreshold: 1024 * 1024,  // bytes a disposed singleton may retain (default 1 MB)
  settleMs: 1000,                              // grace period after a flow ends (default 1000)
  contextGetter: () => currentLensmcpContext(), // optional: returns { sessionId, flowId?, requestId?, originNodeId? }
});

// 2. Track a container. Patching is in-place, non-destructive, and
//    idempotent (re-tracking the same instance is a no-op).
class SessionCache {
  private readonly entries = new Map<string, Session>();

  constructor(instanceId: string) {
    trackContainer({
      ownerInstanceId: instanceId, // identifies the owning instance
      fieldName: 'entries',        // the field being tracked
      container: this.entries,     // the Map / Set / Array to instrument
    });
  }
}

// 3. Bracket a flow. Mutations between start and settle are attributed to
//    this flow; owners that grew past the threshold and stayed resident are
//    flagged and emitted as `memory-retention` events.
onFlowStart('req-42');
// ... handle request: cache.entries.set(...) etc.
const flaggedOwnerIds = onFlowSettled('req-42'); // string[] of suspected-leak owners
```

`trackContainer` only patches `Map`, `Set`, and `Array` instances — anything else is ignored. Tracked mutations are: `set` / `delete` / `clear` (Map), `add` / `delete` / `clear` (Set), and `push` / `splice` (Array).

## API

- **`configureMemoryTracker(opts)`** — Set the active session id, the event `sink`, the optional `contextGetter`, and detector thresholds (`leakItemThreshold`, `staleGenerationBytesThreshold`, `settleMs`). Returns the resolved options. Must be called before events are emitted.
- **`trackContainer({ ownerInstanceId, fieldName, container })`** — Instrument a `Map`/`Set`/`Array` in place so each mutation updates its owner record and emits a `memory-mutation` event. Idempotent and non-destructive.
- **`onFlowStart(flowId)`** — Snapshot every owner's item count at the start of a flow.
- **`onFlowSettled(flowId)`** — Compare each owner's attributed growth against `leakItemThreshold`; emit `memory-retention` for owners that grew and stayed resident. Returns the flagged owner ids.
- **`checkStaleGeneration({ logicalId, instanceId, generation, retainedBytes })`** — On singleton disposal, emit `singleton-stale-generation` when retained bytes exceed `staleGenerationBytesThreshold`. Returns whether it flagged.
- **`allOwners()` / `getOwner(ownerId)` / `ownerKey(ownerInstanceId, fieldName)`** — Inspect the owner registry: list owner snapshots, fetch one record, or compute an owner key (`<ownerInstanceId>::<fieldName>`).
- **`emitMutation` / `emitRetention` / `emitStaleGeneration`** — Low-level emit helpers that build each memory `BaseEvent` and push it to the sink (used internally; exported for advanced hosts).
- **`activeMemoryOptions()` / `currentMemoryContext()` / `resetMemoryTracker()`** — Read the resolved options, read the current flow context, or reset tracker state (test hook).
- **`markFlowStart` / `recordFlowDelta` / `flowGrowth` / `ensureOwner` / `resetOwners`** — Lower-level owner/flow primitives behind the detectors.
- **Types** — `MemoryEventSink`, `MemoryContext`, `ResolvedMemoryOptions`, `MemoryOwnerSnapshot`, `MemoryContainerKind`, `MemoryMutationOperation`, and the `TrackContainerArgs` input.

## How it fits

`@lensmcp/memory-tracker` is enabled by `@lensmcp/nest-instrumentation` via its `memory` option (the node instrumentation does the same). The host supplies the `sessionId`, the event `sink`, and a `contextGetter` bound to its own request context, and brackets each request with `onFlowStart` / `onFlowSettled`.

Every event is a `@lensmcp/protocol-types` `BaseEvent` with `source` and `category` of `memory`, carrying one of three payload kinds:

- `memory-mutation` — `debug` severity; per-mutation before/after counts and an estimated byte delta.
- `memory-retention` — `warning` severity; the "memory leak suspected" signal (owner id, flow, growth) surfaced in the lens.
- `singleton-stale-generation` — `warning` severity; a disposed singleton generation still retaining bytes.

These events flow through the host's sink to the LensMCP bus, where the lens renders them in the **"memory leak suspected"** channel.

---

Part of [LensMCP](https://github.com/kiwiapps-ltd/lensmcp). Apache-2.0.
