@pencil-agent/nano-mem 0.0.1 → 1.0.2
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/README.md +5 -5
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -1
- package/dist/config.js.map +1 -1
- package/dist/consolidation.d.ts.map +1 -1
- package/dist/consolidation.js +25 -8
- package/dist/consolidation.js.map +1 -1
- package/dist/engine.d.ts +7 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +181 -65
- package/dist/engine.js.map +1 -1
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +70 -1
- package/dist/extension.js.map +1 -1
- package/dist/extraction.d.ts.map +1 -1
- package/dist/extraction.js +19 -2
- package/dist/extraction.js.map +1 -1
- package/dist/full-insights.d.ts.map +1 -1
- package/dist/full-insights.js +6 -5
- package/dist/full-insights.js.map +1 -1
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +56 -24
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/insights-html.js +1 -1
- package/dist/insights-html.js.map +1 -1
- package/dist/linking.js +1 -1
- package/dist/linking.js.map +1 -1
- package/dist/scoring.d.ts +10 -1
- package/dist/scoring.d.ts.map +1 -1
- package/dist/scoring.js +33 -0
- package/dist/scoring.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +40 -2
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +22 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +22 -10
- package/dist/update.js.map +1 -1
- package/package.json +2 -3
- package/src/cli.ts +1 -1
- package/src/config.ts +27 -1
- package/src/consolidation.ts +33 -9
- package/src/engine.ts +218 -118
- package/src/extension.ts +77 -1
- package/src/extraction.ts +19 -2
- package/src/full-insights.ts +6 -5
- package/src/i18n.ts +62 -24
- package/src/index.ts +4 -1
- package/src/insights-html.ts +1 -1
- package/src/linking.ts +1 -1
- package/src/scoring.ts +48 -1
- package/src/store.ts +41 -2
- package/src/types.ts +25 -2
- package/src/update.ts +22 -10
package/src/engine.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { getConfig, type NanomemConfig } from "./config.js";
|
|
|
12
12
|
import { consolidateEpisodes } from "./consolidation.js";
|
|
13
13
|
import { utilityEntry, utilityWork } from "./eviction.js";
|
|
14
14
|
import { extractMemories, extractWork } from "./extraction.js";
|
|
15
|
+
import type { PromptSet } from "./i18n.js";
|
|
15
16
|
import { PROMPTS } from "./i18n.js";
|
|
16
17
|
import { getRelatedSummaries, linkNewEntry } from "./linking.js";
|
|
17
18
|
import { evictExpiredEntries, evictExpiredWork, filterByScope, filterPII } from "./privacy.js";
|
|
18
|
-
import { extractTags, pickTop, scoreEntry, scoreEpisode, scoreWorkEntry, tagOverlap } from "./scoring.js";
|
|
19
|
+
import { extractTags, pickTop, scoreEntry, scoreEpisode, scoreWorkEntry, tagOverlap, tierEntries } from "./scoring.js";
|
|
19
20
|
import {
|
|
20
21
|
loadEntries,
|
|
21
22
|
loadEpisodes,
|
|
@@ -115,6 +116,7 @@ export class NanoMemEngine {
|
|
|
115
116
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
116
117
|
goal: sessionGoal || extracted.goal,
|
|
117
118
|
summary: filterPII(extracted.summary),
|
|
119
|
+
detail: extracted.detail ? filterPII(extracted.detail) : undefined,
|
|
118
120
|
project,
|
|
119
121
|
tags: extractTags(`${extracted.goal} ${extracted.summary}`),
|
|
120
122
|
importance: 6,
|
|
@@ -173,7 +175,7 @@ export class NanoMemEngine {
|
|
|
173
175
|
return newEntries;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
// ─── Retrieval & Injection
|
|
178
|
+
// ─── Retrieval & Injection (Progressive Recall) ────────────
|
|
177
179
|
|
|
178
180
|
async getMemoryInjection(project: string, contextTags: string[], scope?: MemoryScope): Promise<string> {
|
|
179
181
|
const [allKnowledge, allLessons, allPrefs, allFacets, allEpisodes, allWork] = await Promise.all([
|
|
@@ -192,79 +194,159 @@ export class NanoMemEngine {
|
|
|
192
194
|
const work = this.filterAndCleanWork(allWork, scope);
|
|
193
195
|
const episodes = filterByScope(allEpisodes, scope);
|
|
194
196
|
|
|
195
|
-
const totalBudget = this.cfg.tokenBudget;
|
|
196
|
-
const b = this.cfg.budget;
|
|
197
197
|
const hl = this.cfg.halfLife;
|
|
198
198
|
const sw = this.cfg.scoreWeights;
|
|
199
|
+
const pr = this.cfg.progressiveRecall;
|
|
199
200
|
const p = PROMPTS[this.cfg.locale] ?? PROMPTS.en;
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
230
|
-
|
|
202
|
+
// Tier all MemoryEntry categories
|
|
203
|
+
const tieredKnowledge = tierEntries(knowledge, project, contextTags, hl, sw, pr);
|
|
204
|
+
const tieredLessons = tierEntries(lessons, project, contextTags, hl, sw, pr);
|
|
205
|
+
const tieredPrefs = tierEntries(prefs, project, contextTags, hl, sw, pr);
|
|
206
|
+
const tieredFacets = tierEntries(facets, project, contextTags, hl, sw, pr);
|
|
207
|
+
|
|
208
|
+
// Budget calculation
|
|
209
|
+
const totalChars = this.cfg.tokenBudget * 4;
|
|
210
|
+
const activeChars = Math.floor(totalChars * pr.budgetActive);
|
|
211
|
+
const cueChars = Math.floor(totalChars * pr.budgetCue);
|
|
212
|
+
|
|
213
|
+
// Entry length helpers
|
|
214
|
+
const activeLen = (e: MemoryEntry) =>
|
|
215
|
+
(e.name?.length || 0) + (e.summary?.length || 0) + (e.detail?.length || 0) + 30;
|
|
216
|
+
const cueLen = (e: MemoryEntry) => (e.name?.length || 0) + (e.summary?.length || 0) + 30;
|
|
217
|
+
const scoreFn = (e: MemoryEntry) => scoreEntry(e, project, contextTags, hl, sw);
|
|
218
|
+
|
|
219
|
+
// Active tier: pick top entries with full detail, split budget across categories
|
|
220
|
+
const activeBudgetPer = Math.floor(activeChars / 4);
|
|
221
|
+
const activeKnowledge = pickTop(tieredKnowledge.active, scoreFn, activeLen, activeBudgetPer);
|
|
222
|
+
const activeLessons = pickTop(tieredLessons.active, scoreFn, activeLen, activeBudgetPer);
|
|
223
|
+
const activePrefs = pickTop(tieredPrefs.active, scoreFn, activeLen, activeBudgetPer);
|
|
224
|
+
const activeFacets = pickTop(tieredFacets.active, scoreFn, activeLen, activeBudgetPer);
|
|
225
|
+
|
|
226
|
+
// Cue tier: pick top entries with name + summary + id, split budget across categories
|
|
227
|
+
const cueBudgetPer = Math.floor(cueChars / 6); // 6 = knowledge + lessons + prefs + facets + episodes + work
|
|
228
|
+
const cueKnowledge = pickTop(tieredKnowledge.cue, scoreFn, cueLen, cueBudgetPer);
|
|
229
|
+
const cueLessons = pickTop(tieredLessons.cue, scoreFn, cueLen, cueBudgetPer);
|
|
230
|
+
const cuePrefs = pickTop(tieredPrefs.cue, scoreFn, cueLen, cueBudgetPer);
|
|
231
|
+
const cueFacets = pickTop(tieredFacets.cue, scoreFn, cueLen, cueBudgetPer);
|
|
232
|
+
|
|
233
|
+
// Episodes and Work use their existing scoring for cue layer
|
|
231
234
|
const topEpisodes = pickTop(
|
|
232
235
|
episodes,
|
|
233
236
|
(ep) => scoreEpisode(ep, project, contextTags, hl, sw),
|
|
234
|
-
(ep) => ep.summary.length,
|
|
235
|
-
|
|
237
|
+
(ep) => ep.summary.length + 30,
|
|
238
|
+
cueBudgetPer,
|
|
236
239
|
);
|
|
237
|
-
|
|
238
240
|
const topWork = pickTop(
|
|
239
241
|
work,
|
|
240
242
|
(w) => scoreWorkEntry(w, project, contextTags, hl, sw),
|
|
241
|
-
(w) => w.goal.length + w.summary.length,
|
|
242
|
-
|
|
243
|
+
(w) => w.goal.length + w.summary.length + 30,
|
|
244
|
+
cueBudgetPer,
|
|
243
245
|
);
|
|
244
246
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
// Reinforce all recalled entries (Active + Cue) via spaced repetition
|
|
248
|
+
const allRecalledKnowledge = [...activeKnowledge, ...cueKnowledge];
|
|
249
|
+
const allRecalledLessons = [...activeLessons, ...cueLessons];
|
|
250
|
+
const allRecalledPrefs = [...activePrefs, ...cuePrefs];
|
|
251
|
+
const allRecalledFacets = [...activeFacets, ...cueFacets];
|
|
252
|
+
|
|
253
|
+
await this.reinforceEntries(allRecalledKnowledge, allKnowledge, this.knowledgePath);
|
|
254
|
+
await this.reinforceEntries(allRecalledLessons, allLessons, this.lessonsPath);
|
|
255
|
+
await this.reinforceEntries(allRecalledPrefs, allPrefs, this.preferencesPath);
|
|
256
|
+
await this.reinforceEntries(allRecalledFacets, allFacets, this.facetsPath);
|
|
249
257
|
await this.reinforceWork(topWork, allWork);
|
|
250
258
|
|
|
259
|
+
// Optional reconsolidation for low-relevance recalled entries
|
|
251
260
|
if (this.llmFn) {
|
|
252
|
-
await this.reconsolidateIfNeeded(
|
|
253
|
-
await this.reconsolidateIfNeeded(
|
|
261
|
+
await this.reconsolidateIfNeeded(allRecalledKnowledge, contextTags, allKnowledge);
|
|
262
|
+
await this.reconsolidateIfNeeded(allRecalledLessons, contextTags, allLessons);
|
|
254
263
|
}
|
|
255
264
|
|
|
256
|
-
return this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
265
|
+
return this.buildProgressiveInjectionText(
|
|
266
|
+
{
|
|
267
|
+
knowledge: activeKnowledge,
|
|
268
|
+
lessons: activeLessons,
|
|
269
|
+
preferences: activePrefs,
|
|
270
|
+
facets: activeFacets,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
knowledge: cueKnowledge,
|
|
274
|
+
lessons: cueLessons,
|
|
275
|
+
preferences: cuePrefs,
|
|
276
|
+
facets: cueFacets,
|
|
277
|
+
episodes: topEpisodes,
|
|
278
|
+
work: topWork,
|
|
279
|
+
},
|
|
263
280
|
allKnowledge,
|
|
264
281
|
p,
|
|
265
282
|
);
|
|
266
283
|
}
|
|
267
284
|
|
|
285
|
+
// ─── Progressive Recall Tools ───────────────────────────────
|
|
286
|
+
|
|
287
|
+
/** Retrieve a single entry by ID (for recall_memory tool) */
|
|
288
|
+
async getEntryById(id: string): Promise<MemoryEntry | null> {
|
|
289
|
+
const paths = [this.knowledgePath, this.lessonsPath, this.preferencesPath, this.facetsPath];
|
|
290
|
+
for (const path of paths) {
|
|
291
|
+
const entries = await loadEntries(path);
|
|
292
|
+
const entry = entries.find((e) => e.id === id);
|
|
293
|
+
if (entry) return entry;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Reinforce a single entry by ID (bump accessCount, lastAccessed, strength) */
|
|
299
|
+
async reinforceEntryById(id: string): Promise<boolean> {
|
|
300
|
+
const pathConfigs = [
|
|
301
|
+
{ path: this.knowledgePath, max: this.cfg.maxEntries.knowledge },
|
|
302
|
+
{ path: this.lessonsPath, max: this.cfg.maxEntries.lessons },
|
|
303
|
+
{ path: this.preferencesPath, max: this.cfg.maxEntries.preferences },
|
|
304
|
+
{ path: this.facetsPath, max: this.cfg.maxEntries.facets },
|
|
305
|
+
];
|
|
306
|
+
const hl = this.cfg.halfLife;
|
|
307
|
+
const ew = this.cfg.evictionWeights;
|
|
308
|
+
|
|
309
|
+
for (const { path, max } of pathConfigs) {
|
|
310
|
+
const entries = await loadEntries(path);
|
|
311
|
+
const entry = entries.find((e) => e.id === id);
|
|
312
|
+
if (entry) {
|
|
313
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
314
|
+
entry.lastAccessed = new Date().toISOString();
|
|
315
|
+
entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
|
|
316
|
+
await saveEntries(path, entries, max, (e) => utilityEntry(e, hl, ew));
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Full-text search across ALL entries including dormant (for search_all_memories tool) */
|
|
324
|
+
async searchAllEntries(query: string, limit = 10): Promise<MemoryEntry[]> {
|
|
325
|
+
const tags = extractTags(query);
|
|
326
|
+
const queryLower = query.toLowerCase();
|
|
327
|
+
|
|
328
|
+
const [knowledge, lessons, prefs, facets] = await Promise.all([
|
|
329
|
+
loadEntries(this.knowledgePath),
|
|
330
|
+
loadEntries(this.lessonsPath),
|
|
331
|
+
loadEntries(this.preferencesPath),
|
|
332
|
+
loadEntries(this.facetsPath),
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const all = [...knowledge, ...lessons, ...prefs, ...facets];
|
|
336
|
+
|
|
337
|
+
return all
|
|
338
|
+
.map((e) => {
|
|
339
|
+
const nameMatch = (e.name || "").toLowerCase().includes(queryLower) ? 2 : 0;
|
|
340
|
+
const summaryMatch = (e.summary || "").toLowerCase().includes(queryLower) ? 1 : 0;
|
|
341
|
+
const tagMatch = tagOverlap(e.tags, tags);
|
|
342
|
+
return { entry: e, score: nameMatch + summaryMatch + tagMatch };
|
|
343
|
+
})
|
|
344
|
+
.filter((x) => x.score > 0)
|
|
345
|
+
.sort((a, b) => b.score - a.score)
|
|
346
|
+
.slice(0, limit)
|
|
347
|
+
.map((x) => x.entry);
|
|
348
|
+
}
|
|
349
|
+
|
|
268
350
|
// ─── Stats ───────────────────────────────────────────────────
|
|
269
351
|
|
|
270
352
|
async getStats(): Promise<{
|
|
@@ -404,8 +486,8 @@ export class NanoMemEngine {
|
|
|
404
486
|
.map((e) => ({
|
|
405
487
|
entry: e,
|
|
406
488
|
weight: calcWeight(e),
|
|
407
|
-
trigger: e.facetData?.kind === "pattern" ? e.facetData.trigger : e.
|
|
408
|
-
behavior: e.facetData?.kind === "pattern" ? e.facetData.behavior : e.
|
|
489
|
+
trigger: e.facetData?.kind === "pattern" ? e.facetData.trigger : (e.summary || e.detail || "").slice(0, 50),
|
|
490
|
+
behavior: e.facetData?.kind === "pattern" ? e.facetData.behavior : (e.summary || e.detail || ""),
|
|
409
491
|
}))
|
|
410
492
|
.sort((a, b) => b.weight - a.weight);
|
|
411
493
|
|
|
@@ -416,7 +498,7 @@ export class NanoMemEngine {
|
|
|
416
498
|
return {
|
|
417
499
|
entry: e,
|
|
418
500
|
weight: calcWeight(e, !isResolved),
|
|
419
|
-
problem: e.facetData?.kind === "struggle" ? e.facetData.problem : e.
|
|
501
|
+
problem: e.facetData?.kind === "struggle" ? e.facetData.problem : (e.summary || e.detail || ""),
|
|
420
502
|
attempts: e.facetData?.kind === "struggle" ? e.facetData.attempts : [],
|
|
421
503
|
solution: e.facetData?.kind === "struggle" ? e.facetData.solution : "",
|
|
422
504
|
resolved: isResolved,
|
|
@@ -462,7 +544,7 @@ export class NanoMemEngine {
|
|
|
462
544
|
const input = JSON.stringify({
|
|
463
545
|
patterns: patterns.slice(0, 5).map((pa) => ({ trigger: pa.trigger, behavior: pa.behavior })),
|
|
464
546
|
struggles: struggles.slice(0, 5).map((s) => ({ problem: s.problem, resolved: s.resolved })),
|
|
465
|
-
lessons: lessons.slice(0, 5).map((l) => l.content),
|
|
547
|
+
lessons: lessons.slice(0, 5).map((l) => l.summary || l.detail || l.content || ""),
|
|
466
548
|
});
|
|
467
549
|
const raw = await this.llmFn(p.insightsRecommendationSystem, input);
|
|
468
550
|
const cleaned = raw
|
|
@@ -609,13 +691,17 @@ export class NanoMemEngine {
|
|
|
609
691
|
const overlap = tagOverlap(entry.tags, contextTags);
|
|
610
692
|
if (overlap >= 0.3) continue;
|
|
611
693
|
try {
|
|
694
|
+
const memText = entry.detail || entry.summary || entry.content || "";
|
|
612
695
|
const updated = await this.llmFn(
|
|
613
696
|
p.reconsolidationSystem,
|
|
614
|
-
`Original memory: ${
|
|
697
|
+
`Original memory: ${memText}\n\nCurrent context tags: ${contextTags.join(", ")}`,
|
|
615
698
|
);
|
|
616
699
|
if (updated && updated.length > 10) {
|
|
617
|
-
|
|
618
|
-
entry.
|
|
700
|
+
const trimmed = updated.trim();
|
|
701
|
+
entry.detail = trimmed;
|
|
702
|
+
entry.content = trimmed;
|
|
703
|
+
entry.summary = trimmed.length <= 150 ? trimmed : `${trimmed.slice(0, 147)}...`;
|
|
704
|
+
entry.tags = extractTags(trimmed);
|
|
619
705
|
}
|
|
620
706
|
} catch {
|
|
621
707
|
/* graceful degradation */
|
|
@@ -623,74 +709,88 @@ export class NanoMemEngine {
|
|
|
623
709
|
}
|
|
624
710
|
}
|
|
625
711
|
|
|
626
|
-
private
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
sectionPatterns: string;
|
|
641
|
-
sectionStruggles: string;
|
|
642
|
-
injectionHeader: string;
|
|
643
|
-
memoryBehavior: string;
|
|
712
|
+
private buildProgressiveInjectionText(
|
|
713
|
+
active: {
|
|
714
|
+
knowledge: MemoryEntry[];
|
|
715
|
+
lessons: MemoryEntry[];
|
|
716
|
+
preferences: MemoryEntry[];
|
|
717
|
+
facets: MemoryEntry[];
|
|
718
|
+
},
|
|
719
|
+
cue: {
|
|
720
|
+
knowledge: MemoryEntry[];
|
|
721
|
+
lessons: MemoryEntry[];
|
|
722
|
+
preferences: MemoryEntry[];
|
|
723
|
+
facets: MemoryEntry[];
|
|
724
|
+
episodes: Episode[];
|
|
725
|
+
work: WorkEntry[];
|
|
644
726
|
},
|
|
727
|
+
allKnowledge: MemoryEntry[],
|
|
728
|
+
p: PromptSet,
|
|
645
729
|
): string {
|
|
646
730
|
const sections: string[] = [];
|
|
647
731
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
732
|
+
// ── Active tier: full detail ──
|
|
733
|
+
const activeLines: string[] = [];
|
|
734
|
+
|
|
735
|
+
const formatActiveEntry = (e: MemoryEntry): string => {
|
|
736
|
+
const related = getRelatedSummaries(e, allKnowledge, 2);
|
|
737
|
+
const suffix = related.length ? ` [→ ${related.join("; ")}]` : "";
|
|
738
|
+
return `- [ID: ${e.id}] **${e.name || "—"}**: ${e.summary || ""}\n ${e.detail || ""}${suffix}`;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const formatActiveFacet = (e: MemoryEntry): string => {
|
|
742
|
+
if (e.facetData?.kind === "pattern") {
|
|
743
|
+
return `- [ID: ${e.id}] **${e.name || "—"}**: When ${e.facetData.trigger} → ${e.facetData.behavior}\n ${e.detail || ""}`;
|
|
744
|
+
}
|
|
745
|
+
if (e.facetData?.kind === "struggle") {
|
|
746
|
+
return `- [ID: ${e.id}] **${e.name || "—"}**: ${e.facetData.problem}\n Tried: ${e.facetData.attempts.join(", ")} | Solved: ${e.facetData.solution}\n ${e.detail || ""}`;
|
|
747
|
+
}
|
|
748
|
+
return formatActiveEntry(e);
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
for (const e of active.lessons) activeLines.push(formatActiveEntry(e));
|
|
752
|
+
for (const e of active.knowledge) activeLines.push(formatActiveEntry(e));
|
|
753
|
+
for (const e of active.preferences) activeLines.push(formatActiveEntry(e));
|
|
754
|
+
for (const e of active.facets) activeLines.push(formatActiveFacet(e));
|
|
755
|
+
|
|
756
|
+
if (activeLines.length) {
|
|
757
|
+
sections.push(`### ${p.sectionActiveMemories}\n${activeLines.join("\n")}`);
|
|
658
758
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
759
|
+
|
|
760
|
+
// ── Cue tier: name + summary + id ──
|
|
761
|
+
const cueLines: string[] = [];
|
|
762
|
+
|
|
763
|
+
const formatCueEntry = (e: MemoryEntry): string =>
|
|
764
|
+
`- [ID: ${e.id}] [${e.type}] **${e.name || "—"}**: ${e.summary || ""}`;
|
|
765
|
+
|
|
766
|
+
const formatCueFacet = (e: MemoryEntry): string => {
|
|
767
|
+
if (e.facetData?.kind === "pattern") {
|
|
768
|
+
return `- [ID: ${e.id}] [pattern] **${e.name || "—"}**: When ${e.facetData.trigger} → ${e.facetData.behavior}`;
|
|
769
|
+
}
|
|
770
|
+
if (e.facetData?.kind === "struggle") {
|
|
771
|
+
return `- [ID: ${e.id}] [struggle] **${e.name || "—"}**: ${e.facetData.problem}`;
|
|
772
|
+
}
|
|
773
|
+
return formatCueEntry(e);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
for (const e of cue.lessons) cueLines.push(formatCueEntry(e));
|
|
777
|
+
for (const e of cue.knowledge) cueLines.push(formatCueEntry(e));
|
|
778
|
+
for (const e of cue.preferences) cueLines.push(formatCueEntry(e));
|
|
779
|
+
for (const e of cue.facets) cueLines.push(formatCueFacet(e));
|
|
780
|
+
|
|
781
|
+
// Episodes in cue layer (no ID-based recall)
|
|
782
|
+
for (const ep of cue.episodes) {
|
|
783
|
+
cueLines.push(
|
|
784
|
+
`- [${ep.date}] ${ep.project}: ${ep.summary}${ep.userGoal ? ` (Goal: ${ep.userGoal})` : ""}`,
|
|
662
785
|
);
|
|
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
786
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
787
|
+
// Work in cue layer
|
|
788
|
+
for (const w of cue.work) {
|
|
789
|
+
cueLines.push(`- [${w.created.slice(0, 10)}] ${w.goal}: ${w.summary}`);
|
|
671
790
|
}
|
|
672
791
|
|
|
673
|
-
|
|
674
|
-
|
|
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")}`);
|
|
792
|
+
if (cueLines.length) {
|
|
793
|
+
sections.push(`### ${p.sectionMemoryCues}\n*${p.memoryCueHint}*\n${cueLines.join("\n")}`);
|
|
694
794
|
}
|
|
695
795
|
|
|
696
796
|
if (!sections.length) return "";
|
package/src/extension.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { writeFileSync } from "node:fs";
|
|
11
11
|
import { basename } from "node:path";
|
|
12
|
+
import { Type } from "@sinclair/typebox";
|
|
12
13
|
import type { ExtensionAPI, ExtensionContext } from "@pencil-agent/nano-pencil";
|
|
13
14
|
import { NanoMemEngine } from "./engine.js";
|
|
14
15
|
import { renderFullInsightsHtml } from "./full-insights-html.js";
|
|
@@ -251,7 +252,7 @@ export default function nanomemExtension(pi: ExtensionAPI) {
|
|
|
251
252
|
ctx.ui.notify("NanoMem: no matching memories found", "info");
|
|
252
253
|
return;
|
|
253
254
|
}
|
|
254
|
-
for (const e of results.slice(0, 10)) ctx.ui.notify(`[${e.type}] ${e.content.slice(0, 80)}`, "info");
|
|
255
|
+
for (const e of results.slice(0, 10)) ctx.ui.notify(`[${e.type}] ${(e.summary || e.detail || e.content || "").slice(0, 80)}`, "info");
|
|
255
256
|
},
|
|
256
257
|
});
|
|
257
258
|
|
|
@@ -287,4 +288,79 @@ export default function nanomemExtension(pi: ExtensionAPI) {
|
|
|
287
288
|
ctx.ui.notify(`NanoMem: Insights report written to ${outputPath}`, "info");
|
|
288
289
|
},
|
|
289
290
|
});
|
|
291
|
+
|
|
292
|
+
// ─── Progressive Recall Agent Tools ─────────────────────────
|
|
293
|
+
|
|
294
|
+
pi.registerTool({
|
|
295
|
+
name: "recall_memory",
|
|
296
|
+
label: "Recall Memory",
|
|
297
|
+
description:
|
|
298
|
+
"Retrieve full details of a specific memory entry by its ID. " +
|
|
299
|
+
"Use this when you see a memory cue (name + summary) in context and need the complete information. " +
|
|
300
|
+
"The ID looks like k_101, l_42, p_7, etc.",
|
|
301
|
+
parameters: Type.Object({
|
|
302
|
+
id: Type.String({ description: "The memory entry ID shown in [ID: xxx] cues" }),
|
|
303
|
+
}),
|
|
304
|
+
async execute(_toolCallId, params) {
|
|
305
|
+
const entry = await engine.getEntryById(params.id);
|
|
306
|
+
if (!entry) {
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text" as const, text: `No memory found with ID: ${params.id}` }],
|
|
309
|
+
details: undefined,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Reinforce the entry (spaced repetition: bump accessCount, lastAccessed, strength)
|
|
314
|
+
await engine.reinforceEntryById(params.id);
|
|
315
|
+
|
|
316
|
+
const name = entry.name || "Untitled";
|
|
317
|
+
const summary = entry.summary || "";
|
|
318
|
+
const detail = entry.detail || entry.content || "";
|
|
319
|
+
const lines = [`[${entry.type}] ${name}`];
|
|
320
|
+
if (summary) lines.push("", summary);
|
|
321
|
+
if (detail && detail !== summary) lines.push("", detail);
|
|
322
|
+
if (entry.tags?.length) lines.push("", `Tags: ${entry.tags.join(", ")}`);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
326
|
+
details: undefined,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.registerTool({
|
|
332
|
+
name: "search_all_memories",
|
|
333
|
+
label: "Search All Memories",
|
|
334
|
+
description:
|
|
335
|
+
"Search across ALL stored memories including dormant ones not shown in the current context. " +
|
|
336
|
+
"Use this to find relevant past knowledge, lessons, preferences, or decisions " +
|
|
337
|
+
"when the memory cues in context don't cover what you need.",
|
|
338
|
+
parameters: Type.Object({
|
|
339
|
+
query: Type.String({ description: "Search keywords or phrase" }),
|
|
340
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 10)", default: 10 })),
|
|
341
|
+
}),
|
|
342
|
+
async execute(_toolCallId, params) {
|
|
343
|
+
const limit = params.limit ?? 10;
|
|
344
|
+
const results = await engine.searchAllEntries(params.query, limit);
|
|
345
|
+
if (!results.length) {
|
|
346
|
+
return {
|
|
347
|
+
content: [{ type: "text" as const, text: `No memories found matching: ${params.query}` }],
|
|
348
|
+
details: undefined,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const lines = [`Found ${results.length} memories for "${params.query}":\n`];
|
|
353
|
+
for (const e of results) {
|
|
354
|
+
const name = e.name || "Untitled";
|
|
355
|
+
const summary = e.summary || e.content?.slice(0, 100) || "";
|
|
356
|
+
lines.push(`- [ID: ${e.id}] [${e.type}] **${name}**: ${summary}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push("", "Use `recall_memory` with an ID to get full details.");
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
362
|
+
details: undefined,
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
});
|
|
290
366
|
}
|
package/src/extraction.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { NanomemConfig } from "./config.js";
|
|
8
8
|
import { PROMPTS } from "./i18n.js";
|
|
9
|
+
import { deriveNameFromContent, deriveSummaryFromContent } from "./store.js";
|
|
9
10
|
import type { ExtractedItem, ExtractedWork, LlmFn } from "./types.js";
|
|
10
11
|
|
|
11
12
|
export async function extractMemories(
|
|
@@ -25,7 +26,17 @@ async function extractWithLLM(conversation: string, cfg: NanomemConfig, llmFn: L
|
|
|
25
26
|
.replace(/```json?\n?/g, "")
|
|
26
27
|
.replace(/```/g, "")
|
|
27
28
|
.trim();
|
|
28
|
-
|
|
29
|
+
const items = JSON.parse(cleaned) as ExtractedItem[];
|
|
30
|
+
// Normalize: ensure name/summary/detail are populated (backward compat with old LLM responses)
|
|
31
|
+
return items.map((item) => {
|
|
32
|
+
const detail = item.detail || item.content || "";
|
|
33
|
+
return {
|
|
34
|
+
...item,
|
|
35
|
+
name: item.name || deriveNameFromContent(detail),
|
|
36
|
+
summary: item.summary || deriveSummaryFromContent(detail),
|
|
37
|
+
detail,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
29
40
|
} catch {
|
|
30
41
|
return extractHeuristic(conversation);
|
|
31
42
|
}
|
|
@@ -40,7 +51,12 @@ function extractHeuristic(text: string): ExtractedItem[] {
|
|
|
40
51
|
// Skip very short or already seen content
|
|
41
52
|
if (trimmed.length < 8 || seen.has(trimmed.toLowerCase())) return;
|
|
42
53
|
seen.add(trimmed.toLowerCase());
|
|
43
|
-
items.push({
|
|
54
|
+
items.push({
|
|
55
|
+
type,
|
|
56
|
+
name: deriveNameFromContent(trimmed),
|
|
57
|
+
summary: deriveSummaryFromContent(trimmed),
|
|
58
|
+
detail: trimmed,
|
|
59
|
+
});
|
|
44
60
|
};
|
|
45
61
|
|
|
46
62
|
// Lesson patterns: errors, fixes, bugs, warnings, failures
|
|
@@ -148,5 +164,6 @@ function extractWorkHeuristic(text: string): ExtractedWork | null {
|
|
|
148
164
|
return {
|
|
149
165
|
goal: lines[0]?.slice(0, 100) ?? "unknown task",
|
|
150
166
|
summary: `Session with ${lines.length} exchanges`,
|
|
167
|
+
detail: lines.slice(0, 20).join("\n"),
|
|
151
168
|
};
|
|
152
169
|
}
|
package/src/full-insights.ts
CHANGED
|
@@ -132,8 +132,8 @@ function buildPatternsAndStruggles(facets: MemoryEntry[]): { patterns: PatternIn
|
|
|
132
132
|
.map((e) => ({
|
|
133
133
|
entry: e,
|
|
134
134
|
weight: calcWeight(e),
|
|
135
|
-
trigger: e.facetData?.kind === "pattern" ? e.facetData.trigger : e.content.slice(0, 50),
|
|
136
|
-
behavior: e.facetData?.kind === "pattern" ? e.facetData.behavior : e.content,
|
|
135
|
+
trigger: e.facetData?.kind === "pattern" ? e.facetData.trigger : (e.summary || e.detail || e.content || "").slice(0, 50),
|
|
136
|
+
behavior: e.facetData?.kind === "pattern" ? e.facetData.behavior : (e.summary || e.detail || e.content || ""),
|
|
137
137
|
}))
|
|
138
138
|
.sort((a, b) => b.weight - a.weight);
|
|
139
139
|
const struggles: StruggleInsight[] = struggleEntries
|
|
@@ -142,7 +142,7 @@ function buildPatternsAndStruggles(facets: MemoryEntry[]): { patterns: PatternIn
|
|
|
142
142
|
return {
|
|
143
143
|
entry: e,
|
|
144
144
|
weight: calcWeight(e, !isResolved),
|
|
145
|
-
problem: e.facetData?.kind === "struggle" ? e.facetData.problem : e.content,
|
|
145
|
+
problem: e.facetData?.kind === "struggle" ? e.facetData.problem : (e.summary || e.detail || e.content || ""),
|
|
146
146
|
attempts: e.facetData?.kind === "struggle" ? e.facetData.attempts : [],
|
|
147
147
|
solution: e.facetData?.kind === "struggle" ? e.facetData.solution : "",
|
|
148
148
|
resolved: isResolved,
|
|
@@ -202,7 +202,8 @@ function fallbackWins(struggles: StruggleInsight[], lessons: MemoryEntry[]): Ful
|
|
|
202
202
|
}
|
|
203
203
|
const sortLessons = [...lessons].sort((a, b) => b.importance * (b.accessCount + 1) - a.importance * (a.accessCount + 1));
|
|
204
204
|
for (const l of sortLessons.slice(0, 3)) {
|
|
205
|
-
|
|
205
|
+
const text = l.summary || l.detail || l.content || "";
|
|
206
|
+
wins.push({ title: text.slice(0, 60), description: text });
|
|
206
207
|
}
|
|
207
208
|
return wins.slice(0, 8);
|
|
208
209
|
}
|
|
@@ -349,7 +350,7 @@ export async function buildFullInsightsReport(
|
|
|
349
350
|
const context = {
|
|
350
351
|
patterns: patterns.slice(0, 5).map((x) => ({ trigger: x.trigger, behavior: x.behavior })),
|
|
351
352
|
struggles: struggles.slice(0, 8).map((s) => ({ problem: s.problem, resolved: s.resolved, attempts: s.attempts, solution: s.solution })),
|
|
352
|
-
lessons: topLessons.slice(0, 5).map((l) => l.content),
|
|
353
|
+
lessons: topLessons.slice(0, 5).map((l) => l.summary || l.detail || l.content || ""),
|
|
353
354
|
projectAreas: projectAreas.map((a) => ({ name: a.name, sessionCount: a.sessionCount, firstSummary: a.description })),
|
|
354
355
|
topTools: toolsChart.rows.slice(0, 5),
|
|
355
356
|
errorsSummary: errChart.rows.slice(0, 5).map((r) => r.label),
|