import type { Clock, Fs } from './io' import { readJson, writeJson } from './io' export type Layer = 'episodic' | 'semantic' | 'emotional' | 'procedural' | 'reflection' export interface Pad { p: number a: number d: number } export interface Memory { id: number layer: Layer text: string /** 重要度 1–10 */ importance: number source: string createdAt: number lastAccess: number accessCount: number emotion?: Pad } export interface MemoryDoc { items: Memory[] nextId: number /** 自上次鞏固以來累積的重要度 */ pending: number lastConsolidated: number /** 已被反思涵蓋的最大記憶編號 */ reflectedUpTo?: number } export const HALF_LIFE_H = 72 export const TOP_K = 6 export const WEIGHTS = { recency: 1, importance: 1, relevance: 2, emotion: 0.5 } /** 累積重要度達此值就鞏固 */ export const CONSOLIDATE_THRESHOLD = 30 /** 保留率低於此值即遺忘 */ export const FORGET_BELOW = 0.1 /** 穩定度基準(小時) */ const BASE_STABILITY_H = 24 const HOUR = 3600_000 /** 穩定度(小時)隨重要度與取用次數成長 */ export const stability = (m: Pick) => BASE_STABILITY_H * m.importance * (1 + Math.log1p(m.accessCount)) /** 保留率:自最近一次取用起算 */ export function retention(m: Memory, now: number): number { const h = Math.max(0, now - m.lastAccess) / HOUR return Math.exp(-h / stability(m)) } const tokens = (s: string) => { const out = new Set() const latin = s.toLowerCase().match(/[a-z0-9]+/g) ?? [] for (const w of latin) out.add(w) const cjk = s.replace(/[^一-鿿]/g, '') for (let i = 0; i < cjk.length; i++) { out.add(cjk[i]!) if (i + 1 < cjk.length) out.add(cjk.slice(i, i + 2)) } return out } function relevance(query: string, text: string): number { const q = tokens(query) if (!q.size) return 0 const t = tokens(text) let hit = 0 for (const w of q) if (t.has(w)) hit++ return hit / q.size } function moodFit(a?: Pad, b?: Pad): number { if (!a || !b) return 0 const dist = Math.hypot(a.p - b.p, a.a - b.a, a.d - b.d) / Math.hypot(2, 2, 2) return 1 - dist } export function score(m: Memory, query: string, now: number, mood?: Pad): number { const recency = Math.pow(0.5, Math.max(0, now - m.lastAccess) / HOUR / HALF_LIFE_H) return ( WEIGHTS.recency * recency + WEIGHTS.importance * (m.importance / 10) + WEIGHTS.relevance * relevance(query, m.text) + WEIGHTS.emotion * moodFit(m.emotion, mood) ) } /** 疑似金鑰的字串在寫入前遮蔽 */ export function redact(s: string): string { return s .replace(/\b(sk|pk|ghp|gho|xox[bap]|AKIA)[-_A-Za-z0-9]{12,}/g, '[已過濾]') .replace(/\b[A-Za-z0-9+/_-]{32,}\b/g, '[已過濾]') } export type Summarize = (items: Memory[]) => string /** 預設摘要:不呼叫模型,直接串接重點;實際反思文字可由外部注入模型版 */ const defaultSummarize: Summarize = items => items .slice(0, 5) .map(i => i.text) .join(';') export interface MemoryStore { add(input: { layer: Layer; text: string; importance: number; source: string; emotion?: Pad }): Promise /** 檢索前 6 筆,並更新取用紀錄 */ recall(query: string, mood?: Pad): Promise /** 累積達門檻、會話結束或手動 reflect 時鞏固 */ consolidate(opts?: { force?: boolean; summarize?: Summarize }): Promise<{ ran: boolean; forgotten: number; reflection?: Memory }> all(): Promise pending(): Promise } export function createMemory(fs: Fs, clock: Clock, path: string): MemoryStore { const load = () => readJson(fs, path, { items: [], nextId: 1, pending: 0, lastConsolidated: 0 }) const save = (d: MemoryDoc) => writeJson(fs, path, d) return { async add(i) { const d = await load() const now = clock() const m: Memory = { id: d.nextId++, layer: i.layer, text: redact(i.text), importance: Math.min(10, Math.max(1, i.importance)), source: i.source, createdAt: now, lastAccess: now, accessCount: 0, emotion: i.emotion, } d.items.push(m) d.pending += m.importance await save(d) return m }, async recall(query, mood) { const d = await load() const now = clock() const top = d.items .map(m => ({ m, s: score(m, query, now, mood) })) .sort((a, b) => b.s - a.s || b.m.id - a.m.id) .slice(0, TOP_K) .map(x => x.m) for (const m of top) { m.lastAccess = now m.accessCount++ } if (top.length) await save(d) return top }, async consolidate(opts = {}) { const d = await load() const now = clock() if (!opts.force && d.pending < CONSOLIDATE_THRESHOLD) return { ran: false, forgotten: 0 } // 反思:把上次鞏固後新增、尚未被反思涵蓋的記憶濃縮成一筆 const fresh = d.items.filter(m => m.layer !== 'reflection' && m.id > (d.reflectedUpTo ?? 0)) let reflection: Memory | undefined if (fresh.length) { const top = [...fresh].sort((a, b) => b.importance - a.importance || b.id - a.id) reflection = { id: d.nextId++, layer: 'reflection', text: redact((opts.summarize ?? defaultSummarize)(top)), importance: Math.min(10, Math.max(...top.map(t => t.importance)) + 1), source: `reflect:${top.map(t => t.id).join(',')}`, createdAt: now, lastAccess: now, accessCount: 0, } d.items.push(reflection) } d.reflectedUpTo = d.nextId - 1 // 遺忘:保留率過低的記憶淘汰(反思本身不在本輪淘汰) const before = d.items.length d.items = d.items.filter(m => m === reflection || retention(m, now) >= FORGET_BELOW) d.pending = 0 d.lastConsolidated = now await save(d) return { ran: true, forgotten: before - d.items.length, reflection } }, async all() { return (await load()).items }, async pending() { return (await load()).pending }, } }