{"version":3,"file":"recallTracking.cjs","sources":["../../../src/memory/recallTracking.ts"],"sourcesContent":["/**\n * Recall tracking — Phase 2.\n *\n * Lightweight adaptation of upstream\n * `extensions/memory-core/src/short-term-promotion.ts::recordShortTermRecalls`.\n * Upstream stores recalls in a JSON file under `memory/.dreams/`; we store\n * them in a Postgres table `agent_memory_recalls`. Schema captures what the\n * future Phase 3 dreaming/promotion algorithm will need:\n *   - which memory row was surfaced (`memory_id`)\n *   - the query that surfaced it (raw + SHA-256 hash for dedupe)\n *   - hybrid score at the time of recall\n *   - the day bucket (for per-day dedupe / frequency counting)\n *   - the recorded timestamp\n *\n * Best-effort: failures never block memory_search. The caller fires\n * {@link RecallTracker.record} without awaiting the result and ignores errors.\n */\nimport { createHash } from 'crypto';\nimport type { Pool } from 'pg';\n\nexport interface RecallTracker {\n  /** Record that the given memory ids were surfaced to the model for a query. */\n  record(params: RecallRecordParams): Promise<void>;\n  /** Backend-specific schema migration. Idempotent. */\n  migrate(): Promise<void>;\n}\n\nexport interface RecallRecordParams {\n  agentId: string;\n  query: string;\n  hits: Array<{ id: string; path: string; score: number }>;\n  nowMs?: number;\n}\n\nexport const RECALL_TABLE = 'agent_memory_recalls';\n\nfunction hashQuery(query: string): string {\n  return createHash('sha256')\n    .update(query.trim().toLowerCase())\n    .digest('hex')\n    .slice(0, 32);\n}\n\nfunction dayBucket(nowMs: number): string {\n  const d = new Date(nowMs);\n  const y = d.getUTCFullYear();\n  const m = String(d.getUTCMonth() + 1).padStart(2, '0');\n  const day = String(d.getUTCDate()).padStart(2, '0');\n  return `${y}-${m}-${day}`;\n}\n\nexport class PgvectorRecallTracker implements RecallTracker {\n  constructor(\n    private readonly pool: Pool,\n    private readonly table: string = RECALL_TABLE\n  ) {}\n\n  async migrate(): Promise<void> {\n    // [recall-tracking] debug: create table + indexes if missing\n    await this.pool.query(`\n      CREATE TABLE IF NOT EXISTS ${this.table} (\n        id           BIGSERIAL   PRIMARY KEY,\n        agent_id     TEXT        NOT NULL,\n        memory_id    TEXT        NOT NULL,\n        memory_path  TEXT        NOT NULL,\n        query        TEXT        NOT NULL,\n        query_hash   TEXT        NOT NULL,\n        score        DOUBLE PRECISION NOT NULL,\n        day_bucket   TEXT        NOT NULL,\n        recorded_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n      )\n    `);\n    await this.pool.query(\n      `CREATE INDEX IF NOT EXISTS ${this.table}_agent_day_idx ON ${this.table} (agent_id, day_bucket)`\n    );\n    await this.pool.query(\n      `CREATE INDEX IF NOT EXISTS ${this.table}_memory_idx ON ${this.table} (agent_id, memory_id)`\n    );\n    await this.pool.query(\n      `CREATE UNIQUE INDEX IF NOT EXISTS ${this.table}_dedupe_idx\n       ON ${this.table} (agent_id, memory_id, query_hash, day_bucket)`\n    );\n  }\n\n  async record(params: RecallRecordParams): Promise<void> {\n    if (!params.agentId || !params.query.trim() || params.hits.length === 0)\n      return;\n    const nowMs = params.nowMs ?? Date.now();\n    const qhash = hashQuery(params.query);\n    const bucket = dayBucket(nowMs);\n\n    // [recall-tracking] debug: upsert one row per (agent, memory, query, day)\n    // Upstream dedupes per-day per-query so repeated searches don't inflate counts.\n    const values: string[] = [];\n    const args: unknown[] = [];\n    let i = 1;\n    for (const hit of params.hits) {\n      values.push(\n        `($${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, NOW())`\n      );\n      args.push(\n        params.agentId,\n        hit.id,\n        hit.path,\n        params.query,\n        qhash,\n        hit.score,\n        bucket\n      );\n    }\n    const sql = `\n      INSERT INTO ${this.table}\n        (agent_id, memory_id, memory_path, query, query_hash, score, day_bucket, recorded_at)\n      VALUES ${values.join(', ')}\n      ON CONFLICT (agent_id, memory_id, query_hash, day_bucket) DO UPDATE\n      SET score       = GREATEST(${this.table}.score, EXCLUDED.score),\n          recorded_at = NOW()\n    `;\n    await this.pool.query(sql, args);\n  }\n}\n\n/** No-op tracker — used when recall tracking is disabled or the backend isn't pgvector. */\nexport class NullRecallTracker implements RecallTracker {\n  async record(): Promise<void> {}\n  async migrate(): Promise<void> {}\n}\n"],"names":["createHash"],"mappings":"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"}