{"version":3,"file":"semantic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/semantic-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 * Extra candidates pulled from `similar()` on a SCOPED recall before the\n * scope filter runs — the driver's ranking spans every scope in the\n * index, so a bare top-`k` can come back entirely foreign. Mirrors the\n * episodic / procedural tiers' overscan constant.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Shape persisted per semantic memory in the cache driver. The vector\n * itself is stored by the driver's own index (passed via\n * `set({ vector })`), so it is not duplicated in the value.\n */\ntype StoredMemory = {\n  id: string;\n  text: string;\n  /** Isolation key the entry was written under; absent = the shared pool. */\n  scope?: string;\n  metadata?: Record<string, unknown>;\n};\n\n/**\n * Semantic recall tier (memory core M1).\n *\n * Owns: embedding remembered text, writing it to a `@warlock.js/cache`\n * driver with `set({ vector })`, and retrieving by cosine similarity via\n * the driver's `similar()`. Does NOT own: the similarity algorithm or\n * the ANN index — those belong to the cache driver. This mirrors the\n * delegation model of `middleware/builtins/semantic-cache.ts`: memory is\n * embedding-agnostic and store-agnostic, gluing an {@link EmbedderContract}\n * to a {@link CacheDriver}.\n *\n * The driver may be shared across memory instances, so every key carries\n * the configured `namespace` and recall filters hits to that prefix —\n * foreign entries indexed by another instance never leak into a query.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class SemanticMemory {\n  public constructor(\n    private readonly embedder: EmbedderContract,\n    private readonly store: CacheDriver<any, any>,\n    private readonly namespace: string,\n  ) {}\n\n  /**\n   * Embed the item's text and index it under a namespaced, id-derived\n   * key. Re-remembering the same id overwrites the prior vector +\n   * value (the driver upserts by key).\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 value: StoredMemory = {\n      id,\n      text: item.text,\n      scope: item.scope,\n      metadata: item.metadata,\n    };\n\n    await this.store.set(this.keyFor(id, item.scope), value, { vector });\n  }\n\n  /**\n   * Embed `query`, ask the driver for the `k` nearest entries clearing\n   * `threshold`, and return those within this instance's namespace AND\n   * this call's `scope` as scored {@link RecalledMemory}. Hits indexed\n   * under a different namespace (a shared driver) or a different scope\n   * (another tenant / session) are filtered out here, before the caller\n   * ever sees them — an unscoped recall reads only unscoped entries.\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    // A scoped recall overscans: the driver ranks across every scope in\n    // the index, so a plain top-`k` could be filled entirely by foreign\n    // scopes and starve this one. Pull extra candidates, filter, then cap.\n    const hits = await this.store.similar<StoredMemory>(vector, {\n      topK: scope === undefined ? k : Math.max(k * RECALL_OVERSCAN, k),\n      threshold,\n    });\n\n    const prefix = `${this.namespace}.`;\n\n    return hits\n      .filter(\n        (hit: CacheSimilarHit<StoredMemory>) =>\n          hit.key.startsWith(prefix) && hit.value?.scope === scope,\n      )\n      .map((hit: CacheSimilarHit<StoredMemory>) => ({\n        id: hit.value.id,\n        text: hit.value.text,\n        tier: \"semantic\" as const,\n        score: hit.score,\n        metadata: hit.value.metadata,\n      }))\n      .slice(0, Math.max(0, k));\n  }\n\n  /** Drop every semantic entry written under this instance's namespace. */\n  public async clear(): Promise<void> {\n    await this.store.removeNamespace(this.namespace);\n  }\n\n  /**\n   * Namespaced key for an entry. The cache's `parseKey` normalizes `:`\n   * to `.`, so a dot separator keeps the prefix used here aligned with\n   * the `hit.key` the driver returns from `similar()`.\n   *\n   * A scoped entry gets an extra hashed segment so two scopes writing\n   * identical text (same derived id) don't overwrite each other; the\n   * unscoped key shape is unchanged, so entries written before 4.15.0\n   * still resolve. The hash is a write-separation device only — recall\n   * authorization is the exact `value.scope` equality check, so even a\n   * hash collision cannot widen what a scope can read.\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":";;;;;;;;;AAcA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAiCxB,IAAa,iBAAb,MAA4B;CAC1B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB;EAHiB;EACA;EACA;CAChB;;;;;;CAOH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,QAAsB;GAC1B;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,OAAO,CAAC;CACrE;;;;;;;;;CAUA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAKlD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAsB,QAAQ;GAC1D,MAAM,UAAU,SAAY,IAAI,KAAK,IAAI,IAAI,iBAAiB,CAAC;GAC/D;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,SAAwC;GAC5C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,IAAI;GACX,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;CAC5B;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;;;;;;CAcA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}