{"version":3,"file":"temporalDecay.mjs","sources":["../../../src/memory/temporalDecay.ts"],"sourcesContent":["/**\n * Temporal decay — Phase 2.\n *\n * Ported from upstream `extensions/memory-core/src/memory/temporal-decay.ts`.\n * Ages dated memory files (`memory/YYYY-MM-DD.md`) using exponential decay\n * `multiplier = exp(-ln(2) / halfLifeDays * ageInDays)`. At half-life, the\n * score is exactly halved.\n *\n * Evergreen files (MEMORY.md, memory/topics.md, any non-dated file inside\n * memory/) do NOT decay — they represent durable knowledge and should stay\n * hot regardless of age. This mirrors upstream's `isEvergreenMemoryPath`.\n *\n * Since our pgvector rows carry `createdAt`, we don't need filesystem stat\n * fallback — the row timestamp is authoritative for any file without a\n * date in the path.\n */\n\nexport interface TemporalDecayConfig {\n  enabled: boolean;\n  halfLifeDays: number;\n}\n\nexport const DEFAULT_TEMPORAL_DECAY_CONFIG: TemporalDecayConfig = {\n  enabled: false,\n  halfLifeDays: 30,\n};\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst DATED_MEMORY_PATH_RE = /(?:^|\\/)memory\\/(\\d{4})-(\\d{2})-(\\d{2})\\.md$/;\n\nexport function toDecayLambda(halfLifeDays: number): number {\n  if (!Number.isFinite(halfLifeDays) || halfLifeDays <= 0) return 0;\n  return Math.LN2 / halfLifeDays;\n}\n\nexport function calculateTemporalDecayMultiplier(params: {\n  ageInDays: number;\n  halfLifeDays: number;\n}): number {\n  const lambda = toDecayLambda(params.halfLifeDays);\n  const age = Math.max(0, params.ageInDays);\n  if (lambda <= 0 || !Number.isFinite(age)) return 1;\n  return Math.exp(-lambda * age);\n}\n\nexport function applyTemporalDecayToScore(params: {\n  score: number;\n  ageInDays: number;\n  halfLifeDays: number;\n}): number {\n  return params.score * calculateTemporalDecayMultiplier(params);\n}\n\nfunction normalizePath(p: string): string {\n  return (p ?? '').replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\n/** Parse a date out of `memory/YYYY-MM-DD.md` — returns null on non-match or invalid date. */\nexport function parseMemoryDateFromPath(filePath: string): Date | null {\n  const m = DATED_MEMORY_PATH_RE.exec(normalizePath(filePath));\n  if (!m) return null;\n  const y = Number(m[1]);\n  const mo = Number(m[2]);\n  const d = Number(m[3]);\n  if (!Number.isInteger(y) || !Number.isInteger(mo) || !Number.isInteger(d))\n    return null;\n  const ts = Date.UTC(y, mo - 1, d);\n  const parsed = new Date(ts);\n  if (\n    parsed.getUTCFullYear() !== y ||\n    parsed.getUTCMonth() !== mo - 1 ||\n    parsed.getUTCDate() !== d\n  ) {\n    return null;\n  }\n  return parsed;\n}\n\n/**\n * Evergreen = durable knowledge file that should not decay.\n * - `MEMORY.md` / `memory.md` at root\n * - anything inside `memory/` that is NOT a dated `YYYY-MM-DD.md` file\n */\nexport function isEvergreenMemoryPath(filePath: string): boolean {\n  const n = normalizePath(filePath);\n  if (n === 'MEMORY.md' || n === 'memory.md') return true;\n  if (!n.startsWith('memory/')) return false;\n  return !DATED_MEMORY_PATH_RE.test(n);\n}\n\nfunction ageInDays(timestamp: Date, nowMs: number): number {\n  return Math.max(0, nowMs - timestamp.getTime()) / DAY_MS;\n}\n\nexport interface DecayCandidate {\n  path: string;\n  score: number;\n  createdAt?: Date;\n}\n\n/**\n * Apply temporal decay to a list of memory hits.\n *\n * Priority for the effective timestamp:\n *   1. Dated path (`memory/YYYY-MM-DD.md`) — use the date in the path\n *   2. Otherwise, if the path is evergreen — NO decay\n *   3. Otherwise, use the row's `createdAt`\n */\nexport function applyTemporalDecayToHits<T extends DecayCandidate>(\n  hits: T[],\n  config: Partial<TemporalDecayConfig> = {},\n  nowMs: number = Date.now()\n): T[] {\n  const merged = { ...DEFAULT_TEMPORAL_DECAY_CONFIG, ...config };\n  if (!merged.enabled) return [...hits];\n\n  return hits.map((h) => {\n    const datedTs = parseMemoryDateFromPath(h.path);\n    let ts: Date | null = datedTs;\n    if (!ts) {\n      if (isEvergreenMemoryPath(h.path)) return h;\n      ts = h.createdAt ?? null;\n    }\n    if (!ts) return h;\n    return {\n      ...h,\n      score: applyTemporalDecayToScore({\n        score: h.score,\n        ageInDays: ageInDays(ts, nowMs),\n        halfLifeDays: merged.halfLifeDays,\n      }),\n    };\n  });\n}\n"],"names":[],"mappings":"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"}