{"version":3,"file":"procedural-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/procedural-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n  MemoryItem,\n  RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per procedure. `uses` is the reinforcement counter —\n * how many times the procedure has been remembered/re-affirmed — and\n * feeds the reinforcement half of the blended recall score. The vector\n * lives in the driver's index, so it is not duplicated here.\n */\ntype StoredProcedure = {\n  id: string;\n  text: string;\n  uses: number;\n  /** Isolation key the procedure was written under; absent = the shared pool. */\n  scope?: string;\n  metadata?: Record<string, unknown>;\n};\n\n/**\n * Extra candidates pulled from `similar()` before re-ranking by the\n * reinforcement-blended score and slicing to `k` — reinforcement can\n * promote a well-worn procedure past a slightly-closer one-off, which the\n * raw top-`k` by similarity would miss.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Procedural recall tier (memory core M2).\n *\n * Holds durable *how-to* knowledge — learned procedures, policies, and\n * playbooks — and retrieves the ones relevant to a query, **blended with\n * reinforcement** so procedures that have proven themselves (remembered /\n * re-affirmed more often) outrank one-offs at equal similarity. That\n * reinforcement weighting is the difference from the semantic tier (which\n * treats every fact equally): procedural memory gets *stronger with use*.\n *\n * Reinforcement is explicit and side-effect-free on read: re-remembering\n * a procedure (same id, or same text → same derived id) increments its\n * `uses`, so a caller strengthens a procedure by remembering it again\n * after a successful application. Recall never mutates.\n *\n * Like the other vector tiers it delegates similarity to the\n * `@warlock.js/cache` driver's `similar()`. The blended `score` stays in\n * `[0, 1]` so procedural hits merge and sort alongside the other tiers.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class ProceduralMemory {\n  public constructor(\n    private readonly embedder: EmbedderContract,\n    private readonly store: CacheDriver<any, any>,\n    private readonly namespace: string,\n    private readonly reinforcementWeight: number,\n  ) {}\n\n  /**\n   * Embed the procedure text and index it, incrementing its `uses` when\n   * it already exists (reinforcement) or seeding it at `1` when new.\n   * Metadata on a reinforcing write wins; an omitted metadata keeps the\n   * prior value rather than wiping it.\n   */\n  public async remember(item: MemoryItem): Promise<void> {\n    const id = item.id ?? deriveMemoryId(item.text);\n    const { vector } = await this.embedder.embed(item.text);\n\n    const key = this.keyFor(id, item.scope);\n    const existing = await this.store.get<StoredProcedure>(key);\n    const uses = (existing?.uses ?? 0) + 1;\n\n    const value: StoredProcedure = {\n      id,\n      text: item.text,\n      uses,\n      scope: item.scope,\n      metadata: item.metadata ?? existing?.metadata,\n    };\n\n    await this.store.set(key, value, { vector });\n  }\n\n  /**\n   * Embed `query`, pull the nearest procedures clearing the similarity\n   * `threshold`, then re-rank each by a reinforcement-blended score and\n   * return the top `k`. The similarity floor still gates relevance;\n   * reinforcement only reorders procedures that already cleared it.\n   *\n   * Procedures written under a different `scope` (another tenant /\n   * session) are dropped here, before scoring and slicing, so they can\n   * neither leak nor consume a slot. An unscoped recall reads only\n   * unscoped procedures.\n   */\n  public async recall(\n    query: string,\n    k: number,\n    threshold: number,\n    scope?: string,\n  ): Promise<RecalledMemory[]> {\n    const { vector } = await this.embedder.embed(query);\n\n    const hits = await this.store.similar<StoredProcedure>(vector, {\n      topK: Math.max(k * RECALL_OVERSCAN, k),\n      threshold,\n    });\n\n    const prefix = `${this.namespace}.`;\n\n    return hits\n      .filter(\n        (hit: CacheSimilarHit<StoredProcedure>) =>\n          hit.key.startsWith(prefix) && hit.value?.scope === scope,\n      )\n      .map((hit: CacheSimilarHit<StoredProcedure>) => ({\n        id: hit.value.id,\n        text: hit.value.text,\n        tier: \"procedural\" as const,\n        score: this.blend(hit.score, hit.value.uses),\n        metadata: hit.value.metadata,\n      }))\n      .sort((first, second) => second.score - first.score)\n      .slice(0, k);\n  }\n\n  /** Drop every procedure written under this instance's namespace. */\n  public async clear(): Promise<void> {\n    await this.store.removeNamespace(this.namespace);\n  }\n\n  /**\n   * Combine raw similarity with a saturating reinforcement proxy:\n   * `(1 - w)·similarity + w·(uses / (uses + 1))`. A first-time procedure\n   * contributes `0.5`; each reinforcement nudges it toward `1` with\n   * diminishing returns. With `reinforcementWeight` 0 the score is pure\n   * similarity.\n   */\n  private blend(similarity: number, uses: number): number {\n    const reinforcement = uses / (uses + 1);\n\n    return (\n      (1 - this.reinforcementWeight) * similarity +\n      this.reinforcementWeight * reinforcement\n    );\n  }\n\n  /**\n   * Namespaced key for an entry — dot separator, matching `similar()`\n   * keys, plus a hashed scope segment so reinforcement counters never\n   * cross a scope boundary (one tenant re-affirming a procedure must not\n   * strengthen — or overwrite — another tenant's identical text).\n   * Unscoped keys keep their pre-4.15.0 shape.\n   */\n  private keyFor(id: string, scope?: string): string {\n    return scope === undefined\n      ? `${this.namespace}.${id}`\n      : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n  }\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;AAwBxB,IAAa,mBAAb,MAA8B;CAC5B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB,AAAiB,qBACjB;EAJiB;EACA;EACA;EACA;CAChB;;;;;;;CAQH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK;EACtC,MAAM,WAAW,MAAM,KAAK,MAAM,IAAqB,GAAG;EAC1D,MAAM,QAAQ,UAAU,QAAQ,KAAK;EAErC,MAAM,QAAyB;GAC7B;GACA,MAAM,KAAK;GACX;GACA,OAAO,KAAK;GACZ,UAAU,KAAK,YAAY,UAAU;EACvC;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE,OAAO,CAAC;CAC7C;;;;;;;;;;;;CAaA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAElD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAyB,QAAQ;GAC7D,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC;GACrC;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAA2C;GAC/C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,KAAK,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI;GAC3C,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,CAAC,CACnD,MAAM,GAAG,CAAC;CACf;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;CASA,AAAQ,MAAM,YAAoB,MAAsB;EACtD,MAAM,gBAAgB,QAAQ,OAAO;EAErC,QACG,IAAI,KAAK,uBAAuB,aACjC,KAAK,sBAAsB;CAE/B;;;;;;;;CASA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}