{"version":3,"file":"working-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/working-memory.ts"],"sourcesContent":["import type {\n  MemoryItem,\n  RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * In-run working memory — the volatile scratch tier (memory core M1).\n *\n * Owns: an insertion-ordered buffer of remembered items keyed by id,\n * with overwrite-in-place on a repeated id. Does NOT own: durability,\n * cross-process sharing, embeddings, or similarity — working memory is\n * a plain in-process buffer the orchestrator threads across the turns of\n * a single run.\n *\n * Recall here is not semantic: with no vector index, \"relevant\" reduces\n * to \"recent.\" `recall()` returns the most-recently-remembered items\n * first, each scored on a `[0, 1]` recency proxy so a caller can merge\n * working hits with semantic hits and sort on one `score` field.\n *\n * **Bounded (4.15.0).** The buffer holds at most `maxItems` entries\n * across every scope; the oldest-written entry is evicted on overflow\n * (FIFO). The tier lives in process memory for the lifetime of the\n * `memory()` instance — which the orchestrator resolves once and reuses\n * for every session — so an unbounded buffer was a memory-exhaustion\n * vector for any long-lived, internet-reachable deployment.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class WorkingMemory {\n  /**\n   * Hard ceiling on buffered entries, across all scopes. Enforced on\n   * every `remember()`; see {@link evictOverflow} for the policy.\n   */\n  private readonly maxItems: number;\n\n  public constructor(maxItems: number) {\n    this.maxItems = maxItems;\n  }\n\n  /**\n   * Scoped key → entry. A `Map` preserves insertion order, so iteration\n   * yields oldest-first; recall reverses it for most-recent-first.\n   *\n   * The map key folds in the item's `scope` so two scopes remembering\n   * identical text (same derived id) stay two independent entries\n   * instead of clobbering one another; the entry keeps its logical `id`\n   * and its `scope` so recall can filter and still report the id the\n   * caller knows.\n   */\n  private readonly entries = new Map<\n    string,\n    {\n      id: string;\n      text: string;\n      scope?: string;\n      metadata?: Record<string, unknown>;\n    }\n  >();\n\n  /**\n   * Append an item to the buffer (or overwrite the entry sharing its\n   * id *within the same scope*). Re-inserting an existing key keeps its\n   * original position; delete + set would move it to the end and lie\n   * about recency, so the value is updated in place.\n   *\n   * Overflowing `maxItems` evicts from the front — see\n   * {@link evictOverflow}.\n   */\n  public remember(item: MemoryItem): void {\n    const id = item.id ?? deriveMemoryId(item.text);\n\n    this.entries.set(scopedKey(item.scope, id), {\n      id,\n      text: item.text,\n      scope: item.scope,\n      metadata: item.metadata,\n    });\n\n    this.evictOverflow();\n  }\n\n  /**\n   * Enforce the size bound by dropping oldest-written entries first\n   * (FIFO over the `Map`'s insertion order).\n   *\n   * **Why FIFO, not LRU.** Recall here is a pure recency proxy — it\n   * reverses insertion order and slices the newest `k` — and never\n   * reorders anything, so the front of the buffer is by construction the\n   * region recall reaches last. FIFO therefore evicts exactly the\n   * entries a bounded recall would never have returned. True LRU would\n   * need read-time reordering, which would also rewrite the `score`\n   * every recall reports (a re-read entry would masquerade as freshly\n   * remembered), trading a real correctness property for no gain.\n   *\n   * **Known limitation (documented, not a regression).** The bound is\n   * global, not per-scope: a session writing heavily can push another\n   * session's older entries out of the buffer. That is a recall-quality\n   * degradation on a volatile scratch tier, never a disclosure — the\n   * scope filter in {@link recall} still applies — and a per-scope quota\n   * would not help anyway, since an attacker holding many sessions\n   * evicts through the global bound regardless. Durable recall belongs\n   * in the semantic / episodic tiers.\n   */\n  private evictOverflow(): void {\n    while (this.entries.size > this.maxItems) {\n      const oldest = this.entries.keys().next();\n\n      if (oldest.done) {\n        return;\n      }\n\n      this.entries.delete(oldest.value);\n    }\n  }\n\n  /**\n   * Return up to `k` most-recently-remembered items *within `scope`*,\n   * newest first. The scope match is exact equality (an unscoped recall\n   * sees only unscoped entries) and is applied BEFORE the slice, so a\n   * foreign scope's entries can never consume a slot or leak out.\n   *\n   * The `score` is a linear recency proxy: the newest item scores `1`,\n   * the oldest of the returned slice trends toward `0`. Working memory\n   * ignores any similarity threshold — it has no vector to compare.\n   */\n  public recall(k: number, scope?: string): RecalledMemory[] {\n    const ordered = [...this.entries.values()]\n      .reverse()\n      .filter((entry) => entry.scope === scope);\n\n    const slice = ordered.slice(0, Math.max(0, k));\n\n    return slice.map((entry, index) => ({\n      id: entry.id,\n      text: entry.text,\n      tier: \"working\" as const,\n      score: slice.length <= 1 ? 1 : 1 - index / slice.length,\n      metadata: entry.metadata,\n    }));\n  }\n\n  /** Drop every working-tier entry, across every scope. */\n  public clear(): void {\n    this.entries.clear();\n  }\n}\n\n/**\n * Map key for a buffer entry: the isolation `scope` (empty for the\n * unscoped pool) length-prefixed and joined to the logical id. The\n * length prefix makes the encoding injective — no crafted scope/id pair\n * can collide with a different scope's entry the way a plain `:` join\n * would allow.\n */\nfunction scopedKey(scope: string | undefined, id: string): string {\n  return `${scope?.length ?? 0}:${scope ?? \"\"}:${id}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAA2B;CAOzB,AAAO,YAAY,UAAkB;iCAcV,IAAI,IAQ7B;EArBA,KAAK,WAAW;CAClB;;;;;;;;;;CA+BA,AAAO,SAAS,MAAwB;EACtC,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAE9C,KAAK,QAAQ,IAAI,UAAU,KAAK,OAAO,EAAE,GAAG;GAC1C;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB,CAAC;EAED,KAAK,cAAc;CACrB;;;;;;;;;;;;;;;;;;;;;;;CAwBA,AAAQ,gBAAsB;EAC5B,OAAO,KAAK,QAAQ,OAAO,KAAK,UAAU;GACxC,MAAM,SAAS,KAAK,QAAQ,KAAK,CAAC,CAAC,KAAK;GAExC,IAAI,OAAO,MACT;GAGF,KAAK,QAAQ,OAAO,OAAO,KAAK;EAClC;CACF;;;;;;;;;;;CAYA,AAAO,OAAO,GAAW,OAAkC;EAKzD,MAAM,QAJU,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CACvC,QAAQ,CAAC,CACT,QAAQ,UAAU,MAAM,UAAU,KAEjB,CAAC,CAAC,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;EAE7C,OAAO,MAAM,KAAK,OAAO,WAAW;GAClC,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,MAAM;GACN,OAAO,MAAM,UAAU,IAAI,IAAI,IAAI,QAAQ,MAAM;GACjD,UAAU,MAAM;EAClB,EAAE;CACJ;;CAGA,AAAO,QAAc;EACnB,KAAK,QAAQ,MAAM;CACrB;AACF;;;;;;;;AASA,SAAS,UAAU,OAA2B,IAAoB;CAChE,OAAO,GAAG,OAAO,UAAU,EAAE,GAAG,SAAS,GAAG,GAAG;AACjD"}