import { redactSecrets } from './secrets.ts' import type { Fs } from './types.ts' export type Turn = { role: 'user' | 'assistant'; text: string; at: number } export type InteractionMemory = { recent: Turn[] /** 壓縮後的摘要行與累計關係變化 */ summary: string[] relationDelta: number } export const RECENT_MAX = 30 export const COMPRESS_COUNT = 20 const SUMMARY_MAX = 40 const LINE_MAX = 60 export const empty = (): InteractionMemory => ({ recent: [], summary: [], relationDelta: 0 }) // 互動記憶不存大段程式碼;只留對話文字 export function dialogueOnly(text: string): string { return text .replace(/```[\s\S]*?(?:```|$)/g, ' ') .replace(/`[^`\n]{40,}`/g, ' ') .replace(/[ \t]+/g, ' ') .trim() } export function clean(text: string): string { return redactSecrets(dialogueOnly(text)) } export function add(m: InteractionMemory, turn: Turn, relationDelta: number): InteractionMemory { const text = clean(turn.text) if (!text) return { ...m, relationDelta: m.relationDelta + relationDelta } let next: InteractionMemory = { ...m, recent: [...m.recent, { ...turn, text }], relationDelta: m.relationDelta + relationDelta, } if (next.recent.length > RECENT_MAX) next = compress(next) return next } /** 把最舊的逐字紀錄壓縮成摘要行 */ export function compress(m: InteractionMemory): InteractionMemory { const old = m.recent.slice(0, COMPRESS_COUNT) const lines = old .filter(t => t.role === 'user') .map(t => (t.text.length > LINE_MAX ? t.text.slice(0, LINE_MAX) + '…' : t.text)) return { ...m, recent: m.recent.slice(COMPRESS_COUNT), summary: [...m.summary, ...lines].slice(-SUMMARY_MAX) } } export async function load(fs: Fs, path: string): Promise { const raw = await fs.read(path) if (!raw) return empty() try { return { ...empty(), ...JSON.parse(raw) } } catch { return empty() } } export const save = (fs: Fs, path: string, m: InteractionMemory) => fs.write(path, JSON.stringify(m))