refactor: statusbar、companion、herdr 外掛改名加上 jsc- 前綴
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
f10b168c28
commit
58e2b2d26a
@@ -0,0 +1,201 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user