Files
jsc-marketplace/plugins/jsc-mod-companion/hooks/companion/memory.ts
T
rootandClaude Sonnet 5.5 ea17f1cb2d refactor: jsc-mod 改名 jsc-mod-statusline、jsc-companion 改名 jsc-mod-companion,指令拆成獨立指令
- jsc-mod-statusline:/jsc-mod-statusline:enable、compact、disable
- jsc-mod-companion:/jsc-mod-companion:status、switch、disable、setup、todo、quiet
- 同步更新 plugin.json、marketplace.json 與各 README
- 補上 statusline band 測試缺少的 ui.render 底層處理

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 05:45:52 +00:00

202 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Memory, 'importance' | 'accessCount'>) =>
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<string>()
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<Memory>
/** 檢索前 6 筆,並更新取用紀錄 */
recall(query: string, mood?: Pad): Promise<Memory[]>
/** 累積達門檻、會話結束或手動 reflect 時鞏固 */
consolidate(opts?: { force?: boolean; summarize?: Summarize }): Promise<{ ran: boolean; forgotten: number; reflection?: Memory }>
all(): Promise<Memory[]>
pending(): Promise<number>
}
export function createMemory(fs: Fs, clock: Clock, path: string): MemoryStore {
const load = () => readJson<MemoryDoc>(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
},
}
}