@pencil-agent/nano-mem 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +258 -0
- package/README.md +146 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +48 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidation.d.ts +13 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +111 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/engine.d.ts +67 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +492 -0
- package/dist/engine.js.map +1 -0
- package/dist/eviction.d.ts +16 -0
- package/dist/eviction.d.ts.map +1 -0
- package/dist/eviction.js +22 -0
- package/dist/eviction.js.map +1 -0
- package/dist/extension.d.ts +11 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +264 -0
- package/dist/extension.js.map +1 -0
- package/dist/extraction.d.ts +10 -0
- package/dist/extraction.d.ts.map +1 -0
- package/dist/extraction.js +136 -0
- package/dist/extraction.js.map +1 -0
- package/dist/full-insights-html.d.ts +8 -0
- package/dist/full-insights-html.d.ts.map +1 -0
- package/dist/full-insights-html.js +311 -0
- package/dist/full-insights-html.js.map +1 -0
- package/dist/full-insights.d.ts +21 -0
- package/dist/full-insights.d.ts.map +1 -0
- package/dist/full-insights.js +327 -0
- package/dist/full-insights.js.map +1 -0
- package/dist/i18n.d.ts +50 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +169 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/insights-html.d.ts +8 -0
- package/dist/insights-html.d.ts.map +1 -0
- package/dist/insights-html.js +431 -0
- package/dist/insights-html.js.map +1 -0
- package/dist/linking.d.ts +11 -0
- package/dist/linking.d.ts.map +1 -0
- package/dist/linking.js +40 -0
- package/dist/linking.js.map +1 -0
- package/dist/privacy.d.ts +16 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +52 -0
- package/dist/privacy.js.map +1 -0
- package/dist/scoring.d.ts +25 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +63 -0
- package/dist/scoring.js.map +1 -0
- package/dist/store.d.ts +16 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +68 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +191 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/update.d.ts +14 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +126 -0
- package/dist/update.js.map +1 -0
- package/package.json +60 -0
- package/src/cli.ts +99 -0
- package/src/config.ts +72 -0
- package/src/consolidation.ts +127 -0
- package/src/engine.ts +699 -0
- package/src/eviction.ts +30 -0
- package/src/extension.ts +290 -0
- package/src/extraction.ts +152 -0
- package/src/full-insights-html.ts +342 -0
- package/src/full-insights.ts +396 -0
- package/src/i18n.ts +233 -0
- package/src/index.ts +50 -0
- package/src/insights-html.ts +476 -0
- package/src/linking.ts +43 -0
- package/src/privacy.ts +52 -0
- package/src/scoring.ts +94 -0
- package/src/store.ts +84 -0
- package/src/types.ts +209 -0
- package/src/update.ts +141 -0
package/src/engine.ts
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: NanomemConfig, optional LlmFn
|
|
3
|
+
* [OUTPUT]: NanoMemEngine — unified API for memory CRUD, injection, consolidation
|
|
4
|
+
* [POS]: Facade layer — composes store, scoring, eviction, update, linking, privacy, extraction, consolidation
|
|
5
|
+
*
|
|
6
|
+
* Host products create an engine instance and call its methods.
|
|
7
|
+
* No dependency on any specific AI framework — LLM is pluggable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getConfig, type NanomemConfig } from "./config.js";
|
|
12
|
+
import { consolidateEpisodes } from "./consolidation.js";
|
|
13
|
+
import { utilityEntry, utilityWork } from "./eviction.js";
|
|
14
|
+
import { extractMemories, extractWork } from "./extraction.js";
|
|
15
|
+
import { PROMPTS } from "./i18n.js";
|
|
16
|
+
import { getRelatedSummaries, linkNewEntry } from "./linking.js";
|
|
17
|
+
import { evictExpiredEntries, evictExpiredWork, filterByScope, filterPII } from "./privacy.js";
|
|
18
|
+
import { extractTags, pickTop, scoreEntry, scoreEpisode, scoreWorkEntry, tagOverlap } from "./scoring.js";
|
|
19
|
+
import {
|
|
20
|
+
loadEntries,
|
|
21
|
+
loadEpisodes,
|
|
22
|
+
loadMeta,
|
|
23
|
+
loadWork,
|
|
24
|
+
saveEpisode as persistEpisode,
|
|
25
|
+
saveEntries,
|
|
26
|
+
saveWork,
|
|
27
|
+
writeJson,
|
|
28
|
+
} from "./store.js";
|
|
29
|
+
import { buildFullInsightsReport } from "./full-insights.js";
|
|
30
|
+
import type {
|
|
31
|
+
Episode,
|
|
32
|
+
ExtractedItem,
|
|
33
|
+
FullInsightsReport,
|
|
34
|
+
InsightsReport,
|
|
35
|
+
LlmFn,
|
|
36
|
+
MemoryEntry,
|
|
37
|
+
MemoryScope,
|
|
38
|
+
Meta,
|
|
39
|
+
PatternInsight,
|
|
40
|
+
StruggleInsight,
|
|
41
|
+
WorkEntry,
|
|
42
|
+
} from "./types.js";
|
|
43
|
+
import { applyExtraction } from "./update.js";
|
|
44
|
+
|
|
45
|
+
export class NanoMemEngine {
|
|
46
|
+
readonly cfg: NanomemConfig;
|
|
47
|
+
private llmFn?: LlmFn;
|
|
48
|
+
|
|
49
|
+
private knowledgePath: string;
|
|
50
|
+
private lessonsPath: string;
|
|
51
|
+
private preferencesPath: string;
|
|
52
|
+
private facetsPath: string;
|
|
53
|
+
private workPath: string;
|
|
54
|
+
private metaPath: string;
|
|
55
|
+
private episodesDir: string;
|
|
56
|
+
|
|
57
|
+
constructor(overrides?: Partial<NanomemConfig>, llmFn?: LlmFn) {
|
|
58
|
+
this.cfg = getConfig(overrides);
|
|
59
|
+
this.llmFn = llmFn;
|
|
60
|
+
this.knowledgePath = join(this.cfg.memoryDir, "knowledge.json");
|
|
61
|
+
this.lessonsPath = join(this.cfg.memoryDir, "lessons.json");
|
|
62
|
+
this.preferencesPath = join(this.cfg.memoryDir, "preferences.json");
|
|
63
|
+
this.facetsPath = join(this.cfg.memoryDir, "facets.json");
|
|
64
|
+
this.workPath = join(this.cfg.memoryDir, "work.json");
|
|
65
|
+
this.metaPath = join(this.cfg.memoryDir, "meta.json");
|
|
66
|
+
this.episodesDir = join(this.cfg.memoryDir, "episodes");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setLlmFn(fn: LlmFn): void {
|
|
70
|
+
this.llmFn = fn;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Extraction ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async extractAndStore(conversation: string, project: string): Promise<ExtractedItem[]> {
|
|
76
|
+
const items = await extractMemories(conversation, this.cfg, this.llmFn);
|
|
77
|
+
if (!items.length) return [];
|
|
78
|
+
|
|
79
|
+
const knowledge = await loadEntries(this.knowledgePath);
|
|
80
|
+
const lessons = await loadEntries(this.lessonsPath);
|
|
81
|
+
const prefs = await loadEntries(this.preferencesPath);
|
|
82
|
+
const facets = await loadEntries(this.facetsPath);
|
|
83
|
+
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
const target =
|
|
86
|
+
item.type === "lesson"
|
|
87
|
+
? lessons
|
|
88
|
+
: item.type === "preference"
|
|
89
|
+
? prefs
|
|
90
|
+
: item.type === "pattern" || item.type === "struggle"
|
|
91
|
+
? facets
|
|
92
|
+
: knowledge;
|
|
93
|
+
applyExtraction(target, item, project, this.cfg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const hl = this.cfg.halfLife;
|
|
97
|
+
const ew = this.cfg.evictionWeights;
|
|
98
|
+
await Promise.all([
|
|
99
|
+
saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
|
|
100
|
+
saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
|
|
101
|
+
saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
|
|
102
|
+
saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async extractAndStoreWork(conversation: string, project: string, sessionGoal?: string): Promise<void> {
|
|
109
|
+
const extracted = await extractWork(conversation, this.cfg, this.llmFn);
|
|
110
|
+
if (!extracted || (!extracted.goal && !extracted.summary)) return;
|
|
111
|
+
|
|
112
|
+
const entries = await loadWork(this.workPath);
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const newWork: WorkEntry = {
|
|
115
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
116
|
+
goal: sessionGoal || extracted.goal,
|
|
117
|
+
summary: filterPII(extracted.summary),
|
|
118
|
+
project,
|
|
119
|
+
tags: extractTags(`${extracted.goal} ${extracted.summary}`),
|
|
120
|
+
importance: 6,
|
|
121
|
+
strength: this.cfg.halfLife.work ?? 45,
|
|
122
|
+
created: now,
|
|
123
|
+
eventTime: now,
|
|
124
|
+
accessCount: 0,
|
|
125
|
+
relatedIds: [],
|
|
126
|
+
scope: this.cfg.defaultScope,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
entries.push(newWork);
|
|
130
|
+
await saveWork(this.workPath, entries, this.cfg.maxEntries.work, (w) =>
|
|
131
|
+
utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Episode Management ──────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async saveEpisode(ep: Episode): Promise<void> {
|
|
138
|
+
await persistEpisode(this.episodesDir, ep);
|
|
139
|
+
const meta = await loadMeta(this.metaPath);
|
|
140
|
+
meta.totalSessions++;
|
|
141
|
+
await writeJson(this.metaPath, meta);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async consolidate(): Promise<MemoryEntry[]> {
|
|
145
|
+
const episodes = await loadEpisodes(this.episodesDir);
|
|
146
|
+
const newEntries = await consolidateEpisodes(episodes, this.cfg, this.llmFn);
|
|
147
|
+
if (!newEntries.length) return [];
|
|
148
|
+
|
|
149
|
+
const knowledge = await loadEntries(this.knowledgePath);
|
|
150
|
+
const lessons = await loadEntries(this.lessonsPath);
|
|
151
|
+
|
|
152
|
+
for (const entry of newEntries) {
|
|
153
|
+
linkNewEntry(entry, [...knowledge, ...lessons]);
|
|
154
|
+
if (entry.type === "lesson") lessons.push(entry);
|
|
155
|
+
else knowledge.push(entry);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const hl = this.cfg.halfLife;
|
|
159
|
+
const ew = this.cfg.evictionWeights;
|
|
160
|
+
await Promise.all([
|
|
161
|
+
saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
|
|
162
|
+
saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
for (const ep of episodes) {
|
|
166
|
+
if (ep.consolidated) await persistEpisode(this.episodesDir, ep);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const meta = await loadMeta(this.metaPath);
|
|
170
|
+
meta.lastConsolidation = new Date().toISOString();
|
|
171
|
+
await writeJson(this.metaPath, meta);
|
|
172
|
+
|
|
173
|
+
return newEntries;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Retrieval & Injection ───────────────────────────────────
|
|
177
|
+
|
|
178
|
+
async getMemoryInjection(project: string, contextTags: string[], scope?: MemoryScope): Promise<string> {
|
|
179
|
+
const [allKnowledge, allLessons, allPrefs, allFacets, allEpisodes, allWork] = await Promise.all([
|
|
180
|
+
loadEntries(this.knowledgePath),
|
|
181
|
+
loadEntries(this.lessonsPath),
|
|
182
|
+
loadEntries(this.preferencesPath),
|
|
183
|
+
loadEntries(this.facetsPath),
|
|
184
|
+
loadEpisodes(this.episodesDir),
|
|
185
|
+
loadWork(this.workPath),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const knowledge = this.filterAndCleanEntries(allKnowledge, scope);
|
|
189
|
+
const lessons = this.filterAndCleanEntries(allLessons, scope);
|
|
190
|
+
const prefs = this.filterAndCleanEntries(allPrefs, scope);
|
|
191
|
+
const facets = this.filterAndCleanEntries(allFacets, scope);
|
|
192
|
+
const work = this.filterAndCleanWork(allWork, scope);
|
|
193
|
+
const episodes = filterByScope(allEpisodes, scope);
|
|
194
|
+
|
|
195
|
+
const totalBudget = this.cfg.tokenBudget;
|
|
196
|
+
const b = this.cfg.budget;
|
|
197
|
+
const hl = this.cfg.halfLife;
|
|
198
|
+
const sw = this.cfg.scoreWeights;
|
|
199
|
+
const p = PROMPTS[this.cfg.locale] ?? PROMPTS.en;
|
|
200
|
+
|
|
201
|
+
const charBudget = (ratio: number) => Math.floor(totalBudget * 4 * ratio);
|
|
202
|
+
|
|
203
|
+
const topLessons = pickTop(
|
|
204
|
+
lessons,
|
|
205
|
+
(e) => scoreEntry(e, project, contextTags, hl, sw),
|
|
206
|
+
(e) => e.content.length,
|
|
207
|
+
charBudget(b.lessons),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const topKnowledge = pickTop(
|
|
211
|
+
knowledge,
|
|
212
|
+
(e) => scoreEntry(e, project, contextTags, hl, sw),
|
|
213
|
+
(e) => e.content.length,
|
|
214
|
+
charBudget(b.knowledge),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const topPrefs = pickTop(
|
|
218
|
+
prefs,
|
|
219
|
+
(e) => scoreEntry(e, project, contextTags, hl, sw),
|
|
220
|
+
(e) => e.content.length,
|
|
221
|
+
charBudget(b.preferences),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const topFacets = pickTop(
|
|
225
|
+
facets,
|
|
226
|
+
(e) => scoreEntry(e, project, contextTags, hl, sw),
|
|
227
|
+
(e) => e.content.length,
|
|
228
|
+
charBudget(b.facets),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const topEpisodes = pickTop(
|
|
232
|
+
episodes,
|
|
233
|
+
(ep) => scoreEpisode(ep, project, contextTags, hl, sw),
|
|
234
|
+
(ep) => ep.summary.length,
|
|
235
|
+
charBudget(b.episodes),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const topWork = pickTop(
|
|
239
|
+
work,
|
|
240
|
+
(w) => scoreWorkEntry(w, project, contextTags, hl, sw),
|
|
241
|
+
(w) => w.goal.length + w.summary.length,
|
|
242
|
+
charBudget(b.work),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
await this.reinforceEntries(topLessons, allLessons, this.lessonsPath);
|
|
246
|
+
await this.reinforceEntries(topKnowledge, allKnowledge, this.knowledgePath);
|
|
247
|
+
await this.reinforceEntries(topPrefs, allPrefs, this.preferencesPath);
|
|
248
|
+
await this.reinforceEntries(topFacets, allFacets, this.facetsPath);
|
|
249
|
+
await this.reinforceWork(topWork, allWork);
|
|
250
|
+
|
|
251
|
+
if (this.llmFn) {
|
|
252
|
+
await this.reconsolidateIfNeeded(topKnowledge, contextTags, allKnowledge);
|
|
253
|
+
await this.reconsolidateIfNeeded(topLessons, contextTags, allLessons);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return this.buildInjectionText(
|
|
257
|
+
topLessons,
|
|
258
|
+
topKnowledge,
|
|
259
|
+
topEpisodes,
|
|
260
|
+
topPrefs,
|
|
261
|
+
topWork,
|
|
262
|
+
topFacets,
|
|
263
|
+
allKnowledge,
|
|
264
|
+
p,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Stats ───────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
async getStats(): Promise<{
|
|
271
|
+
knowledge: number;
|
|
272
|
+
lessons: number;
|
|
273
|
+
preferences: number;
|
|
274
|
+
facets: number;
|
|
275
|
+
episodes: number;
|
|
276
|
+
work: number;
|
|
277
|
+
totalSessions: number;
|
|
278
|
+
}> {
|
|
279
|
+
const [knowledge, lessons, prefs, facets, episodes, work, meta] = await Promise.all([
|
|
280
|
+
loadEntries(this.knowledgePath),
|
|
281
|
+
loadEntries(this.lessonsPath),
|
|
282
|
+
loadEntries(this.preferencesPath),
|
|
283
|
+
loadEntries(this.facetsPath),
|
|
284
|
+
loadEpisodes(this.episodesDir),
|
|
285
|
+
loadWork(this.workPath),
|
|
286
|
+
loadMeta(this.metaPath),
|
|
287
|
+
]);
|
|
288
|
+
return {
|
|
289
|
+
knowledge: knowledge.length,
|
|
290
|
+
lessons: lessons.length,
|
|
291
|
+
preferences: prefs.length,
|
|
292
|
+
facets: facets.length,
|
|
293
|
+
episodes: episodes.length,
|
|
294
|
+
work: work.length,
|
|
295
|
+
totalSessions: meta.totalSessions,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Direct Access (for CLI, testing) ────────────────────────
|
|
300
|
+
|
|
301
|
+
async getAllEntries(): Promise<{
|
|
302
|
+
knowledge: MemoryEntry[];
|
|
303
|
+
lessons: MemoryEntry[];
|
|
304
|
+
preferences: MemoryEntry[];
|
|
305
|
+
facets: MemoryEntry[];
|
|
306
|
+
}> {
|
|
307
|
+
return {
|
|
308
|
+
knowledge: await loadEntries(this.knowledgePath),
|
|
309
|
+
lessons: await loadEntries(this.lessonsPath),
|
|
310
|
+
preferences: await loadEntries(this.preferencesPath),
|
|
311
|
+
facets: await loadEntries(this.facetsPath),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getAllWork(): Promise<WorkEntry[]> {
|
|
316
|
+
return loadWork(this.workPath);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async getAllEpisodes(): Promise<Episode[]> {
|
|
320
|
+
return loadEpisodes(this.episodesDir);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async searchEntries(query: string, scope?: MemoryScope): Promise<MemoryEntry[]> {
|
|
324
|
+
const tags = extractTags(query);
|
|
325
|
+
const { knowledge, lessons, preferences, facets } = await this.getAllEntries();
|
|
326
|
+
const all = [...knowledge, ...lessons, ...preferences, ...facets];
|
|
327
|
+
return filterByScope(all, scope)
|
|
328
|
+
.map((e) => ({ entry: e, score: tagOverlap(e.tags, tags) }))
|
|
329
|
+
.filter((x) => x.score > 0)
|
|
330
|
+
.sort((a, b) => b.score - a.score)
|
|
331
|
+
.map((x) => x.entry);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async forgetEntry(id: string): Promise<boolean> {
|
|
335
|
+
const paths = [this.knowledgePath, this.lessonsPath, this.preferencesPath, this.facetsPath];
|
|
336
|
+
for (const path of paths) {
|
|
337
|
+
const entries = await loadEntries(path);
|
|
338
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
339
|
+
if (idx >= 0) {
|
|
340
|
+
entries.splice(idx, 1);
|
|
341
|
+
const hl = this.cfg.halfLife;
|
|
342
|
+
const ew = this.cfg.evictionWeights;
|
|
343
|
+
await saveEntries(path, entries, Infinity, (e) => utilityEntry(e, hl, ew));
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async exportAll(): Promise<{
|
|
351
|
+
knowledge: MemoryEntry[];
|
|
352
|
+
lessons: MemoryEntry[];
|
|
353
|
+
preferences: MemoryEntry[];
|
|
354
|
+
facets: MemoryEntry[];
|
|
355
|
+
work: WorkEntry[];
|
|
356
|
+
episodes: Episode[];
|
|
357
|
+
meta: Meta;
|
|
358
|
+
}> {
|
|
359
|
+
const [knowledge, lessons, preferences, facets, work, episodes, meta] = await Promise.all([
|
|
360
|
+
loadEntries(this.knowledgePath),
|
|
361
|
+
loadEntries(this.lessonsPath),
|
|
362
|
+
loadEntries(this.preferencesPath),
|
|
363
|
+
loadEntries(this.facetsPath),
|
|
364
|
+
loadWork(this.workPath),
|
|
365
|
+
loadEpisodes(this.episodesDir),
|
|
366
|
+
loadMeta(this.metaPath),
|
|
367
|
+
]);
|
|
368
|
+
return { knowledge, lessons, preferences, facets, work, episodes, meta };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Full Insights Report ────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
async generateFullInsights(): Promise<FullInsightsReport> {
|
|
374
|
+
const all = await this.exportAll();
|
|
375
|
+
return buildFullInsightsReport(all, this.llmFn, this.cfg.locale);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── Insights Generation ─────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
async generateInsights(): Promise<InsightsReport> {
|
|
381
|
+
const all = await this.exportAll();
|
|
382
|
+
const stats = {
|
|
383
|
+
knowledge: all.knowledge.length,
|
|
384
|
+
lessons: all.lessons.length,
|
|
385
|
+
preferences: all.preferences.length,
|
|
386
|
+
facets: all.facets.length,
|
|
387
|
+
episodes: all.episodes.length,
|
|
388
|
+
work: all.work.length,
|
|
389
|
+
totalSessions: all.meta.totalSessions,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Separate patterns and struggles from facets
|
|
393
|
+
const patternEntries = all.facets.filter((e) => e.type === "pattern");
|
|
394
|
+
const struggleEntries = all.facets.filter((e) => e.type === "struggle");
|
|
395
|
+
|
|
396
|
+
// Weight calculation: (accessCount + 1) × (importance / 10)
|
|
397
|
+
const calcWeight = (e: MemoryEntry, unresolvedBonus = false): number => {
|
|
398
|
+
const base = (e.accessCount + 1) * (e.importance / 10);
|
|
399
|
+
return unresolvedBonus ? base * 1.5 : base;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Build PatternInsight[]
|
|
403
|
+
const patterns: PatternInsight[] = patternEntries
|
|
404
|
+
.map((e) => ({
|
|
405
|
+
entry: e,
|
|
406
|
+
weight: calcWeight(e),
|
|
407
|
+
trigger: e.facetData?.kind === "pattern" ? e.facetData.trigger : e.content.slice(0, 50),
|
|
408
|
+
behavior: e.facetData?.kind === "pattern" ? e.facetData.behavior : e.content,
|
|
409
|
+
}))
|
|
410
|
+
.sort((a, b) => b.weight - a.weight);
|
|
411
|
+
|
|
412
|
+
// Build StruggleInsight[]
|
|
413
|
+
const struggles: StruggleInsight[] = struggleEntries
|
|
414
|
+
.map((e) => {
|
|
415
|
+
const isResolved = e.facetData?.kind === "struggle" ? !!e.facetData.solution : false;
|
|
416
|
+
return {
|
|
417
|
+
entry: e,
|
|
418
|
+
weight: calcWeight(e, !isResolved),
|
|
419
|
+
problem: e.facetData?.kind === "struggle" ? e.facetData.problem : e.content,
|
|
420
|
+
attempts: e.facetData?.kind === "struggle" ? e.facetData.attempts : [],
|
|
421
|
+
solution: e.facetData?.kind === "struggle" ? e.facetData.solution : "",
|
|
422
|
+
resolved: isResolved,
|
|
423
|
+
};
|
|
424
|
+
})
|
|
425
|
+
.sort((a, b) => {
|
|
426
|
+
// Unresolved first, then by weight
|
|
427
|
+
if (a.resolved !== b.resolved) return a.resolved ? 1 : -1;
|
|
428
|
+
return b.weight - a.weight;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Top lessons and knowledge by importance × accessCount
|
|
432
|
+
const sortByRelevance = (arr: MemoryEntry[]) =>
|
|
433
|
+
[...arr].sort((a, b) => b.importance * (b.accessCount + 1) - a.importance * (a.accessCount + 1));
|
|
434
|
+
|
|
435
|
+
const topLessons = sortByRelevance(all.lessons).slice(0, 10);
|
|
436
|
+
const topKnowledge = sortByRelevance(all.knowledge).slice(0, 10);
|
|
437
|
+
|
|
438
|
+
// Generate recommendations (LLM or rules-based fallback)
|
|
439
|
+
const recommendations = await this.generateRecommendations(patterns, struggles, topLessons);
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
patterns,
|
|
443
|
+
struggles,
|
|
444
|
+
topLessons,
|
|
445
|
+
topKnowledge,
|
|
446
|
+
preferences: all.preferences,
|
|
447
|
+
stats,
|
|
448
|
+
recommendations,
|
|
449
|
+
generatedAt: new Date().toISOString(),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async generateRecommendations(
|
|
454
|
+
patterns: PatternInsight[],
|
|
455
|
+
struggles: StruggleInsight[],
|
|
456
|
+
lessons: MemoryEntry[],
|
|
457
|
+
): Promise<string[]> {
|
|
458
|
+
// Try LLM-based recommendations if available
|
|
459
|
+
if (this.llmFn) {
|
|
460
|
+
try {
|
|
461
|
+
const p = PROMPTS[this.cfg.locale] ?? PROMPTS.en;
|
|
462
|
+
const input = JSON.stringify({
|
|
463
|
+
patterns: patterns.slice(0, 5).map((pa) => ({ trigger: pa.trigger, behavior: pa.behavior })),
|
|
464
|
+
struggles: struggles.slice(0, 5).map((s) => ({ problem: s.problem, resolved: s.resolved })),
|
|
465
|
+
lessons: lessons.slice(0, 5).map((l) => l.content),
|
|
466
|
+
});
|
|
467
|
+
const raw = await this.llmFn(p.insightsRecommendationSystem, input);
|
|
468
|
+
const cleaned = raw
|
|
469
|
+
.replace(/```json?\n?/g, "")
|
|
470
|
+
.replace(/```/g, "")
|
|
471
|
+
.trim();
|
|
472
|
+
const result = JSON.parse(cleaned) as string[];
|
|
473
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
474
|
+
return result.slice(0, 5);
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
// Fall through to rules-based
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Rules-based fallback recommendations
|
|
482
|
+
return this.generateRulesBasedRecommendations(patterns, struggles, lessons);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private generateRulesBasedRecommendations(
|
|
486
|
+
patterns: PatternInsight[],
|
|
487
|
+
struggles: StruggleInsight[],
|
|
488
|
+
lessons: MemoryEntry[],
|
|
489
|
+
): string[] {
|
|
490
|
+
const recommendations: string[] = [];
|
|
491
|
+
const isZh = this.cfg.locale === "zh";
|
|
492
|
+
|
|
493
|
+
// High-weight patterns → automation suggestion
|
|
494
|
+
if (patterns.length > 0) {
|
|
495
|
+
const top = patterns[0]!;
|
|
496
|
+
recommendations.push(
|
|
497
|
+
isZh
|
|
498
|
+
? `你在「${top.trigger}」时稳定执行「${top.behavior}」,考虑将此行为自动化`
|
|
499
|
+
: `You consistently ${top.behavior} when ${top.trigger}. Consider automating this behavior.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Unresolved struggles → systematic review suggestion
|
|
504
|
+
const unresolved = struggles.filter((s) => !s.resolved);
|
|
505
|
+
if (unresolved.length >= 2) {
|
|
506
|
+
recommendations.push(
|
|
507
|
+
isZh
|
|
508
|
+
? `有 ${unresolved.length} 个未解决的问题,建议系统性地逐个攻克`
|
|
509
|
+
: `You have ${unresolved.length} unresolved issues. Consider tackling them systematically.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Recurring tag patterns in struggles → domain-specific review
|
|
514
|
+
const struggleTags = struggles.flatMap((s) => s.entry.tags);
|
|
515
|
+
const tagCounts = struggleTags.reduce(
|
|
516
|
+
(acc, tag) => {
|
|
517
|
+
acc[tag] = (acc[tag] || 0) + 1;
|
|
518
|
+
return acc;
|
|
519
|
+
},
|
|
520
|
+
{} as Record<string, number>,
|
|
521
|
+
);
|
|
522
|
+
const topTag = Object.entries(tagCounts).sort((a, b) => b[1] - a[1])[0];
|
|
523
|
+
if (topTag && topTag[1] >= 3) {
|
|
524
|
+
recommendations.push(
|
|
525
|
+
isZh
|
|
526
|
+
? `「${topTag[0]}」相关的问题反复出现,建议深入学习该领域`
|
|
527
|
+
: `Issues related to "${topTag[0]}" appear frequently. Consider deeper learning in this area.`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Lessons accumulation → expertise recognition
|
|
532
|
+
if (lessons.length >= 5) {
|
|
533
|
+
recommendations.push(
|
|
534
|
+
isZh
|
|
535
|
+
? `你已积累 ${lessons.length} 条经验教训,这是宝贵的知识财富`
|
|
536
|
+
: `You've accumulated ${lessons.length} lessons. This is valuable expertise.`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// No data → encouragement
|
|
541
|
+
if (patterns.length === 0 && struggles.length === 0 && lessons.length === 0) {
|
|
542
|
+
recommendations.push(
|
|
543
|
+
isZh ? `继续使用系统,让它学习你的工作习惯` : `Keep using the system to let it learn your work habits.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return recommendations.slice(0, 5);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ─── Private Helpers ─────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
private filterAndCleanEntries(entries: MemoryEntry[], scope?: MemoryScope): MemoryEntry[] {
|
|
553
|
+
return filterByScope(evictExpiredEntries(entries), scope);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private filterAndCleanWork(entries: WorkEntry[], scope?: MemoryScope): WorkEntry[] {
|
|
557
|
+
return filterByScope(evictExpiredWork(entries), scope);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private async reinforceEntries(recalled: MemoryEntry[], all: MemoryEntry[], savePath: string): Promise<void> {
|
|
561
|
+
const ids = new Set(recalled.map((e) => e.id));
|
|
562
|
+
const now = new Date().toISOString();
|
|
563
|
+
for (const entry of all) {
|
|
564
|
+
if (ids.has(entry.id)) {
|
|
565
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
566
|
+
entry.lastAccessed = now;
|
|
567
|
+
entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const hl = this.cfg.halfLife;
|
|
571
|
+
const ew = this.cfg.evictionWeights;
|
|
572
|
+
await saveEntries(
|
|
573
|
+
savePath,
|
|
574
|
+
all,
|
|
575
|
+
savePath === this.lessonsPath
|
|
576
|
+
? this.cfg.maxEntries.lessons
|
|
577
|
+
: savePath === this.preferencesPath
|
|
578
|
+
? this.cfg.maxEntries.preferences
|
|
579
|
+
: savePath === this.facetsPath
|
|
580
|
+
? this.cfg.maxEntries.facets
|
|
581
|
+
: this.cfg.maxEntries.knowledge,
|
|
582
|
+
(e) => utilityEntry(e, hl, ew),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async reinforceWork(recalled: WorkEntry[], all: WorkEntry[]): Promise<void> {
|
|
587
|
+
const ids = new Set(recalled.map((w) => w.id));
|
|
588
|
+
const now = new Date().toISOString();
|
|
589
|
+
for (const w of all) {
|
|
590
|
+
if (ids.has(w.id)) {
|
|
591
|
+
w.accessCount = (w.accessCount ?? 0) + 1;
|
|
592
|
+
w.lastAccessed = now;
|
|
593
|
+
w.strength = (w.strength || 45) * this.cfg.strengthGrowthFactor;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await saveWork(this.workPath, all, this.cfg.maxEntries.work, (w) =>
|
|
597
|
+
utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private async reconsolidateIfNeeded(
|
|
602
|
+
recalled: MemoryEntry[],
|
|
603
|
+
contextTags: string[],
|
|
604
|
+
_allEntries: MemoryEntry[],
|
|
605
|
+
): Promise<void> {
|
|
606
|
+
if (!this.llmFn) return;
|
|
607
|
+
const p = PROMPTS[this.cfg.locale];
|
|
608
|
+
for (const entry of recalled) {
|
|
609
|
+
const overlap = tagOverlap(entry.tags, contextTags);
|
|
610
|
+
if (overlap >= 0.3) continue;
|
|
611
|
+
try {
|
|
612
|
+
const updated = await this.llmFn(
|
|
613
|
+
p.reconsolidationSystem,
|
|
614
|
+
`Original memory: ${entry.content}\n\nCurrent context tags: ${contextTags.join(", ")}`,
|
|
615
|
+
);
|
|
616
|
+
if (updated && updated.length > 10) {
|
|
617
|
+
entry.content = updated.trim();
|
|
618
|
+
entry.tags = extractTags(entry.content);
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
/* graceful degradation */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private buildInjectionText(
|
|
627
|
+
lessons: MemoryEntry[],
|
|
628
|
+
knowledge: MemoryEntry[],
|
|
629
|
+
episodes: Episode[],
|
|
630
|
+
prefs: MemoryEntry[],
|
|
631
|
+
work: WorkEntry[],
|
|
632
|
+
facets: MemoryEntry[],
|
|
633
|
+
allKnowledge: MemoryEntry[],
|
|
634
|
+
p: {
|
|
635
|
+
sectionLessons: string;
|
|
636
|
+
sectionKnowledge: string;
|
|
637
|
+
sectionEpisodes: string;
|
|
638
|
+
sectionPreferences: string;
|
|
639
|
+
sectionWork: string;
|
|
640
|
+
sectionPatterns: string;
|
|
641
|
+
sectionStruggles: string;
|
|
642
|
+
injectionHeader: string;
|
|
643
|
+
memoryBehavior: string;
|
|
644
|
+
},
|
|
645
|
+
): string {
|
|
646
|
+
const sections: string[] = [];
|
|
647
|
+
|
|
648
|
+
if (lessons.length) {
|
|
649
|
+
sections.push(`### ${p.sectionLessons}\n${lessons.map((e) => `- ${e.content}`).join("\n")}`);
|
|
650
|
+
}
|
|
651
|
+
if (knowledge.length) {
|
|
652
|
+
const lines = knowledge.map((e) => {
|
|
653
|
+
const related = getRelatedSummaries(e, allKnowledge, 2);
|
|
654
|
+
const suffix = related.length ? ` [→ ${related.join("; ")}]` : "";
|
|
655
|
+
return `- ${e.content}${suffix}`;
|
|
656
|
+
});
|
|
657
|
+
sections.push(`### ${p.sectionKnowledge}\n${lines.join("\n")}`);
|
|
658
|
+
}
|
|
659
|
+
if (episodes.length) {
|
|
660
|
+
const lines = episodes.map(
|
|
661
|
+
(ep) => `- [${ep.date}] ${ep.project}: ${ep.summary}${ep.userGoal ? ` (Goal: ${ep.userGoal})` : ""}`,
|
|
662
|
+
);
|
|
663
|
+
sections.push(`### ${p.sectionEpisodes}\n${lines.join("\n")}`);
|
|
664
|
+
}
|
|
665
|
+
if (prefs.length) {
|
|
666
|
+
sections.push(`### ${p.sectionPreferences}\n${prefs.map((e) => `- ${e.content}`).join("\n")}`);
|
|
667
|
+
}
|
|
668
|
+
if (work.length) {
|
|
669
|
+
const lines = work.map((w) => `- [${w.created.slice(0, 10)}] ${w.goal}: ${w.summary}`);
|
|
670
|
+
sections.push(`### ${p.sectionWork}\n${lines.join("\n")}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Facets: patterns and struggles
|
|
674
|
+
const patterns = facets.filter((e) => e.type === "pattern");
|
|
675
|
+
const struggles = facets.filter((e) => e.type === "struggle");
|
|
676
|
+
|
|
677
|
+
if (patterns.length) {
|
|
678
|
+
const lines = patterns.map((e) => {
|
|
679
|
+
if (e.facetData?.kind === "pattern") {
|
|
680
|
+
return `- When ${e.facetData.trigger} → ${e.facetData.behavior}`;
|
|
681
|
+
}
|
|
682
|
+
return `- ${e.content}`;
|
|
683
|
+
});
|
|
684
|
+
sections.push(`### ${p.sectionPatterns}\n${lines.join("\n")}`);
|
|
685
|
+
}
|
|
686
|
+
if (struggles.length) {
|
|
687
|
+
const lines = struggles.map((e) => {
|
|
688
|
+
if (e.facetData?.kind === "struggle") {
|
|
689
|
+
return `- Problem: ${e.facetData.problem} | Tried: ${e.facetData.attempts.join(", ")} | Solved: ${e.facetData.solution}`;
|
|
690
|
+
}
|
|
691
|
+
return `- ${e.content}`;
|
|
692
|
+
});
|
|
693
|
+
sections.push(`### ${p.sectionStruggles}\n${lines.join("\n")}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!sections.length) return "";
|
|
697
|
+
return `## ${p.injectionHeader}\n\n${sections.join("\n\n")}\n\n---\n${p.memoryBehavior}`;
|
|
698
|
+
}
|
|
699
|
+
}
|