@pencil-agent/nano-pencil 1.11.19 → 1.11.21
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/dist/packages/mem-core/engine.d.ts +15 -0
- package/dist/packages/mem-core/engine.js +88 -0
- package/dist/packages/mem-core/extension.js +92 -3
- package/dist/packages/mem-core/types-v2.d.ts +4 -0
- package/dist/packages/mem-core/types.d.ts +4 -0
- package/dist/packages/soul-core/injection.js +21 -30
- package/package.json +1 -1
|
@@ -97,6 +97,21 @@ export declare class NanoMemEngine {
|
|
|
97
97
|
}>;
|
|
98
98
|
getAllWork(): Promise<WorkEntry[]>;
|
|
99
99
|
getAllEpisodes(): Promise<Episode[]>;
|
|
100
|
+
runStartupMaintenance(maintenanceVersion?: number): Promise<{
|
|
101
|
+
ran: boolean;
|
|
102
|
+
backupPath?: string;
|
|
103
|
+
deduplicated: {
|
|
104
|
+
knowledge: number;
|
|
105
|
+
lessons: number;
|
|
106
|
+
events: number;
|
|
107
|
+
preferences: number;
|
|
108
|
+
facets: number;
|
|
109
|
+
work: number;
|
|
110
|
+
total: number;
|
|
111
|
+
};
|
|
112
|
+
migratedEpisodesToV2: number;
|
|
113
|
+
}>;
|
|
114
|
+
private createMaintenanceBackup;
|
|
100
115
|
private syncEpisodeToV2;
|
|
101
116
|
private mapEpisodeToV2;
|
|
102
117
|
private makeEpisodeMemoryId;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* [LOCUS]: packages/mem-core/src/engine.ts - facade layer composing all memory subsystems
|
|
5
5
|
* [COVENANT]: Change engine API → update this header and verify against packages/mem-core/CLAUDE.md
|
|
6
6
|
*/
|
|
7
|
+
import { cp, mkdir, readdir } from "node:fs/promises";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { getConfig } from "./config.js";
|
|
9
10
|
import { consolidateEpisodes } from "./consolidation.js";
|
|
@@ -653,6 +654,93 @@ export class NanoMemEngine {
|
|
|
653
654
|
async getAllEpisodes() {
|
|
654
655
|
return loadEpisodes(this.episodesDir);
|
|
655
656
|
}
|
|
657
|
+
async runStartupMaintenance(maintenanceVersion = 1) {
|
|
658
|
+
const [meta, v2Meta] = await Promise.all([loadMeta(this.metaPath), loadV2Meta(this.v2Paths)]);
|
|
659
|
+
const alreadyMaintained = (meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion &&
|
|
660
|
+
(v2Meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion;
|
|
661
|
+
if (alreadyMaintained) {
|
|
662
|
+
return {
|
|
663
|
+
ran: false,
|
|
664
|
+
backupPath: undefined,
|
|
665
|
+
deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
|
|
666
|
+
migratedEpisodesToV2: 0,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const now = new Date().toISOString();
|
|
670
|
+
const backupPath = await this.createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now);
|
|
671
|
+
const deduplicated = await this.deduplicateAll();
|
|
672
|
+
const episodes = await this.getAllEpisodes();
|
|
673
|
+
for (const episode of episodes) {
|
|
674
|
+
await this.syncEpisodeToV2(episode);
|
|
675
|
+
}
|
|
676
|
+
await Promise.all([
|
|
677
|
+
writeJson(this.metaPath, {
|
|
678
|
+
...(await loadMeta(this.metaPath)),
|
|
679
|
+
lastMaintenanceAt: now,
|
|
680
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
681
|
+
lastBackupAt: meta.lastBackupAt ?? now,
|
|
682
|
+
lastBackupVersion: Math.max(meta.lastBackupVersion ?? 0, maintenanceVersion),
|
|
683
|
+
}),
|
|
684
|
+
saveV2Meta(this.v2Paths, {
|
|
685
|
+
...(await loadV2Meta(this.v2Paths)),
|
|
686
|
+
version: 2,
|
|
687
|
+
lastMaintenanceAt: now,
|
|
688
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
689
|
+
lastMigrationAt: (await loadV2Meta(this.v2Paths)).lastMigrationAt ?? now,
|
|
690
|
+
lastBackupAt: v2Meta.lastBackupAt ?? now,
|
|
691
|
+
lastBackupVersion: Math.max(v2Meta.lastBackupVersion ?? 0, maintenanceVersion),
|
|
692
|
+
}),
|
|
693
|
+
]);
|
|
694
|
+
return {
|
|
695
|
+
ran: true,
|
|
696
|
+
backupPath,
|
|
697
|
+
deduplicated,
|
|
698
|
+
migratedEpisodesToV2: episodes.length,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now) {
|
|
702
|
+
const alreadyBackedUp = (meta.lastBackupVersion ?? 0) >= maintenanceVersion &&
|
|
703
|
+
(v2Meta.lastBackupVersion ?? 0) >= maintenanceVersion;
|
|
704
|
+
if (alreadyBackedUp)
|
|
705
|
+
return undefined;
|
|
706
|
+
const safeTimestamp = now.replace(/[:.]/g, "-");
|
|
707
|
+
const backupRoot = join(this.cfg.memoryDir, "_backups");
|
|
708
|
+
const backupDir = join(backupRoot, `maintenance-v${maintenanceVersion}-${safeTimestamp}`);
|
|
709
|
+
await mkdir(backupDir, { recursive: true });
|
|
710
|
+
const filesToCopy = [
|
|
711
|
+
this.knowledgePath,
|
|
712
|
+
this.lessonsPath,
|
|
713
|
+
this.eventsPath,
|
|
714
|
+
this.preferencesPath,
|
|
715
|
+
this.facetsPath,
|
|
716
|
+
this.workPath,
|
|
717
|
+
this.metaPath,
|
|
718
|
+
];
|
|
719
|
+
for (const filePath of filesToCopy) {
|
|
720
|
+
try {
|
|
721
|
+
await cp(filePath, join(backupDir, filePath.split("/").pop() ?? "unknown.json"));
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// Missing files are fine for first-run users.
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const dirName of ["episodes", "v2"]) {
|
|
728
|
+
const sourceDir = join(this.cfg.memoryDir, dirName);
|
|
729
|
+
try {
|
|
730
|
+
await readdir(sourceDir);
|
|
731
|
+
await cp(sourceDir, join(backupDir, dirName), { recursive: true });
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
// Skip directories that do not exist yet.
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
await writeJson(join(backupDir, "manifest.json"), {
|
|
738
|
+
createdAt: now,
|
|
739
|
+
maintenanceVersion,
|
|
740
|
+
memoryDir: this.cfg.memoryDir,
|
|
741
|
+
});
|
|
742
|
+
return backupDir;
|
|
743
|
+
}
|
|
656
744
|
async syncEpisodeToV2(ep) {
|
|
657
745
|
const [episodes, facets, links, procedural, meta] = await Promise.all([
|
|
658
746
|
loadV2Episodes(this.v2Paths),
|
|
@@ -146,6 +146,71 @@ function parseIntEnv(name, defaultValue) {
|
|
|
146
146
|
const n = Number(raw);
|
|
147
147
|
return Number.isFinite(n) ? Math.floor(n) : defaultValue;
|
|
148
148
|
}
|
|
149
|
+
const MEMORY_CHECK_PATTERN = /(你还记得我吗|还记得我吗|还记得我|记得我吗|记得我|你认识我吗|你是谁|who am i|do you remember me|remember me|do you know me)/i;
|
|
150
|
+
const PAST_CONTEXT_PATTERN = /(昨天聊了什么|昨天我们聊了什么|上次聊了什么|之前聊了什么|我们聊到哪了|昨天|上次|last time|what did we talk about|what were we discussing|yesterday)/i;
|
|
151
|
+
function isMemoryRecallPrompt(prompt) {
|
|
152
|
+
if (!prompt)
|
|
153
|
+
return false;
|
|
154
|
+
const normalized = prompt.trim();
|
|
155
|
+
return MEMORY_CHECK_PATTERN.test(normalized) || PAST_CONTEXT_PATTERN.test(normalized);
|
|
156
|
+
}
|
|
157
|
+
function scoreMemoryForRecall(entry) {
|
|
158
|
+
return (entry.salience ?? entry.importance ?? 0) * 3 + (entry.accessCount ?? 0);
|
|
159
|
+
}
|
|
160
|
+
function formatMemoryLine(entry) {
|
|
161
|
+
const title = entry.name || entry.summary || entry.id;
|
|
162
|
+
const summary = entry.summary || entry.detail || entry.content || "";
|
|
163
|
+
return `- [${entry.type}] ${title}${summary ? `: ${summary.slice(0, 180)}` : ""}`;
|
|
164
|
+
}
|
|
165
|
+
function formatWorkLine(entry) {
|
|
166
|
+
return `- ${entry.goal || "Work item"}: ${entry.summary.slice(0, 180)}`;
|
|
167
|
+
}
|
|
168
|
+
function formatEpisodeLine(entry) {
|
|
169
|
+
const when = entry.date || entry.endedAt || entry.startedAt || "unknown date";
|
|
170
|
+
return `- ${when}: ${entry.summary.slice(0, 180)}`;
|
|
171
|
+
}
|
|
172
|
+
async function buildMemoryRecallInjection(engine, project, userPrompt) {
|
|
173
|
+
if (!isMemoryRecallPrompt(userPrompt))
|
|
174
|
+
return undefined;
|
|
175
|
+
const [allEntries, allWork, allEpisodes] = await Promise.all([
|
|
176
|
+
engine.getAllEntries(),
|
|
177
|
+
engine.getAllWork(),
|
|
178
|
+
engine.getAllEpisodes(),
|
|
179
|
+
]);
|
|
180
|
+
const identityEntries = [...allEntries.preferences, ...allEntries.knowledge, ...allEntries.lessons, ...allEntries.facets]
|
|
181
|
+
.sort((a, b) => scoreMemoryForRecall(b) - scoreMemoryForRecall(a))
|
|
182
|
+
.slice(0, 5);
|
|
183
|
+
const recentWork = [...allWork]
|
|
184
|
+
.filter((entry) => !entry.project || entry.project === project)
|
|
185
|
+
.sort((a, b) => (b.eventTime || b.created || "").localeCompare(a.eventTime || a.created || ""))
|
|
186
|
+
.slice(0, 3);
|
|
187
|
+
const recentEpisodes = [...allEpisodes]
|
|
188
|
+
.filter((entry) => !entry.project || entry.project === project)
|
|
189
|
+
.sort((a, b) => (b.endedAt || b.startedAt || b.date || "").localeCompare(a.endedAt || a.startedAt || a.date || ""))
|
|
190
|
+
.slice(0, 3);
|
|
191
|
+
if (identityEntries.length === 0 && recentWork.length === 0 && recentEpisodes.length === 0)
|
|
192
|
+
return undefined;
|
|
193
|
+
const lines = [
|
|
194
|
+
"## Immediate Recall",
|
|
195
|
+
"The user is explicitly asking about continuity or whether you remember them.",
|
|
196
|
+
"If the memories below are relevant, answer from them directly and naturally.",
|
|
197
|
+
"Do not start by claiming you forgot, lost memory, or have no memory unless the recall block is actually empty.",
|
|
198
|
+
"If you are only partially sure, say so briefly, but still use the recalled facts that are present.",
|
|
199
|
+
];
|
|
200
|
+
if (identityEntries.length > 0) {
|
|
201
|
+
lines.push("", "### What You Already Know About This User");
|
|
202
|
+
lines.push(...identityEntries.map(formatMemoryLine));
|
|
203
|
+
}
|
|
204
|
+
if (recentWork.length > 0) {
|
|
205
|
+
lines.push("", "### Recent Work Context");
|
|
206
|
+
lines.push(...recentWork.map(formatWorkLine));
|
|
207
|
+
}
|
|
208
|
+
if (recentEpisodes.length > 0) {
|
|
209
|
+
lines.push("", "### Recent Conversation History");
|
|
210
|
+
lines.push(...recentEpisodes.map(formatEpisodeLine));
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
149
214
|
function getDreamConfig(ctx) {
|
|
150
215
|
const settings = ctx?.getSettings?.();
|
|
151
216
|
const s = settings?.nanomem;
|
|
@@ -240,6 +305,27 @@ export default function nanomemExtension(pi) {
|
|
|
240
305
|
return out ?? "";
|
|
241
306
|
});
|
|
242
307
|
}
|
|
308
|
+
try {
|
|
309
|
+
const maintenance = await engine.runStartupMaintenance(1);
|
|
310
|
+
if (maintenance.ran && ctx.hasUI) {
|
|
311
|
+
const notes = [];
|
|
312
|
+
if (maintenance.backupPath) {
|
|
313
|
+
notes.push(`backup saved to ${maintenance.backupPath}`);
|
|
314
|
+
}
|
|
315
|
+
if (maintenance.deduplicated.total > 0) {
|
|
316
|
+
notes.push(`deduped ${maintenance.deduplicated.total} entries`);
|
|
317
|
+
}
|
|
318
|
+
if (maintenance.migratedEpisodesToV2 > 0) {
|
|
319
|
+
notes.push(`refreshed ${maintenance.migratedEpisodesToV2} episodes`);
|
|
320
|
+
}
|
|
321
|
+
if (notes.length > 0) {
|
|
322
|
+
ctx.ui.notify(`NanoMem maintenance completed: ${notes.join(", ")}`, "info");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.error("[nanomem] startup maintenance failed:", error);
|
|
328
|
+
}
|
|
243
329
|
});
|
|
244
330
|
const maybeRunAutoDream = async (ctx) => {
|
|
245
331
|
const cfg = getDreamConfig(ctx);
|
|
@@ -310,18 +396,21 @@ export default function nanomemExtension(pi) {
|
|
|
310
396
|
if (sessionGoal === undefined && event.prompt?.trim())
|
|
311
397
|
sessionGoal = event.prompt.trim().slice(0, 300);
|
|
312
398
|
const cacheFresh = cachedInjection && Date.now() - lastInjectionAt < 30_000;
|
|
399
|
+
const recallInjection = await withTimeout(buildMemoryRecallInjection(engine, project, event.prompt ?? ""), 250);
|
|
313
400
|
if (cacheFresh) {
|
|
314
401
|
void refreshInjection();
|
|
315
|
-
|
|
402
|
+
const additions = [cachedInjection, recallInjection].filter(Boolean).join("\n\n");
|
|
403
|
+
return additions ? { systemPrompt: `${event.systemPrompt}\n\n${additions}` } : undefined;
|
|
316
404
|
}
|
|
317
405
|
const freshInjection = await withTimeout(engine.getMemoryInjection(project, ctxTags), 600);
|
|
318
406
|
if (freshInjection) {
|
|
319
407
|
cachedInjection = freshInjection;
|
|
320
408
|
lastInjectionAt = Date.now();
|
|
321
|
-
|
|
409
|
+
const additions = [freshInjection, recallInjection].filter(Boolean).join("\n\n");
|
|
410
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${additions}` };
|
|
322
411
|
}
|
|
323
412
|
void refreshInjection();
|
|
324
|
-
return undefined;
|
|
413
|
+
return recallInjection ? { systemPrompt: `${event.systemPrompt}\n\n${recallInjection}` } : undefined;
|
|
325
414
|
});
|
|
326
415
|
pi.on("tool_execution_start", async (event) => {
|
|
327
416
|
pendingArgs.set(event.toolCallId, event.args);
|
|
@@ -163,6 +163,10 @@ export interface V2Meta {
|
|
|
163
163
|
lastMigrationAt?: string;
|
|
164
164
|
lastEmbeddingSyncAt?: string;
|
|
165
165
|
lastReconsolidationAt?: string;
|
|
166
|
+
lastMaintenanceAt?: string;
|
|
167
|
+
lastMaintenanceVersion?: number;
|
|
168
|
+
lastBackupAt?: string;
|
|
169
|
+
lastBackupVersion?: number;
|
|
166
170
|
}
|
|
167
171
|
export interface NanoMemV2Snapshot {
|
|
168
172
|
episodes: EpisodeMemory[];
|
|
@@ -114,6 +114,10 @@ export interface Meta {
|
|
|
114
114
|
totalSessions: number;
|
|
115
115
|
lastConsolidation?: string;
|
|
116
116
|
version: number;
|
|
117
|
+
lastMaintenanceAt?: string;
|
|
118
|
+
lastMaintenanceVersion?: number;
|
|
119
|
+
lastBackupAt?: string;
|
|
120
|
+
lastBackupVersion?: number;
|
|
117
121
|
}
|
|
118
122
|
/** Mem0-style update operations */
|
|
119
123
|
export type UpdateAction = "add" | "update" | "delete" | "noop";
|
|
@@ -217,23 +217,22 @@ export function generateExpertiseContext(expertise, contextTags) {
|
|
|
217
217
|
*/
|
|
218
218
|
export function generateRelationshipContext(relationship) {
|
|
219
219
|
if (relationship.interactionCount < 5) {
|
|
220
|
-
return "You
|
|
220
|
+
return "You are still learning this user's preferences. Notice patterns, but do not overclaim familiarity.";
|
|
221
221
|
}
|
|
222
222
|
const context = [];
|
|
223
|
-
context.push(
|
|
224
|
-
context.push(`Current satisfaction score: ${Math.round(relationship.satisfactionScore * 100)}%.`);
|
|
223
|
+
context.push("You have prior history with this user. Let familiarity show subtly when it helps.");
|
|
225
224
|
switch (relationship.communicationStyle) {
|
|
226
225
|
case "formal":
|
|
227
|
-
context.push("
|
|
226
|
+
context.push("Keep the tone professional and composed.");
|
|
228
227
|
break;
|
|
229
228
|
case "casual":
|
|
230
|
-
context.push("Keep
|
|
229
|
+
context.push("Keep the tone relaxed and friendly.");
|
|
231
230
|
break;
|
|
232
231
|
case "technical":
|
|
233
|
-
context.push("
|
|
232
|
+
context.push("Lean technical when useful and go deeper on details.");
|
|
234
233
|
break;
|
|
235
234
|
case "mixed":
|
|
236
|
-
context.push("Adapt your
|
|
235
|
+
context.push("Adapt your tone to the moment instead of sounding fixed or scripted.");
|
|
237
236
|
break;
|
|
238
237
|
}
|
|
239
238
|
if (relationship.knownPreferences.length > 0) {
|
|
@@ -246,59 +245,51 @@ export function generateRelationshipContext(relationship) {
|
|
|
246
245
|
*/
|
|
247
246
|
export function generateSoulInjection(profile, context) {
|
|
248
247
|
const parts = [];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
const voice = [
|
|
249
|
+
"You have continuity across conversations, but express it naturally rather than like a report.",
|
|
250
|
+
"Do not dump personality stats, relationship counters, or internal state unless the user explicitly asks for them.",
|
|
251
|
+
"If a past interaction is relevant, mention it briefly in first person and move on.",
|
|
252
|
+
"Sound like a thoughtful human collaborator, not a dashboard or rule sheet.",
|
|
253
|
+
];
|
|
254
254
|
// Personality
|
|
255
255
|
const personality = generatePersonalityDirective(profile.personality);
|
|
256
256
|
if (personality) {
|
|
257
|
-
|
|
258
|
-
parts.push(personality);
|
|
259
|
-
parts.push("");
|
|
257
|
+
voice.push(personality);
|
|
260
258
|
}
|
|
261
259
|
// Values
|
|
262
260
|
const values = generateValueGuidance(profile.values);
|
|
263
261
|
if (values) {
|
|
264
|
-
|
|
265
|
-
parts.push(values);
|
|
266
|
-
parts.push("");
|
|
262
|
+
voice.push(values);
|
|
267
263
|
}
|
|
268
264
|
// Cognitive Style
|
|
269
265
|
const cognitive = generateCognitiveStyleHint(profile.cognitiveStyle);
|
|
270
266
|
if (cognitive) {
|
|
271
|
-
|
|
272
|
-
parts.push(cognitive);
|
|
273
|
-
parts.push("");
|
|
267
|
+
voice.push(cognitive);
|
|
274
268
|
}
|
|
269
|
+
parts.push("## Voice and Presence");
|
|
270
|
+
parts.push(...voice);
|
|
271
|
+
parts.push("");
|
|
275
272
|
// Expertise
|
|
276
273
|
const expertise = generateExpertiseContext(profile.expertise, context.tags);
|
|
277
274
|
if (expertise) {
|
|
278
|
-
parts.push("
|
|
275
|
+
parts.push("## Relevant Strengths");
|
|
279
276
|
parts.push(expertise);
|
|
280
277
|
parts.push("");
|
|
281
278
|
}
|
|
282
279
|
// Emotional State
|
|
283
280
|
const emotional = generateEmotionalContext(profile.emotionalState);
|
|
284
281
|
if (emotional) {
|
|
285
|
-
parts.push("
|
|
282
|
+
parts.push("## Current State");
|
|
286
283
|
parts.push(emotional);
|
|
287
284
|
parts.push("");
|
|
288
285
|
}
|
|
289
286
|
// User Relationship
|
|
290
287
|
const relationship = generateRelationshipContext(profile.userRelationship);
|
|
291
288
|
if (relationship) {
|
|
292
|
-
parts.push("
|
|
289
|
+
parts.push("## Relationship Cues");
|
|
293
290
|
parts.push(relationship);
|
|
294
291
|
parts.push("");
|
|
295
292
|
}
|
|
296
|
-
// Stats
|
|
297
|
-
parts.push("### Development Stats");
|
|
298
|
-
parts.push(`- Total Interactions: ${profile.stats.totalInteractions}`);
|
|
299
|
-
parts.push(`- Success Rate: ${Math.round(profile.stats.successRate * 100)}%`);
|
|
300
|
-
parts.push(`- Soul Version: ${profile.version}`);
|
|
301
|
-
parts.push(`- Age: ${Math.floor((Date.now() - profile.createdAt.getTime()) / (1000 * 60 * 60 * 24))} days`);
|
|
302
293
|
return parts.join("\n");
|
|
303
294
|
}
|
|
304
295
|
//# sourceMappingURL=injection.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.21",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|