@pencil-agent/nano-pencil 1.11.19 → 1.11.20
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 +13 -0
- package/dist/packages/mem-core/engine.js +37 -0
- package/dist/packages/mem-core/extension.js +89 -3
- package/dist/packages/mem-core/types-v2.d.ts +2 -0
- package/dist/packages/mem-core/types.d.ts +2 -0
- package/dist/packages/soul-core/injection.js +21 -30
- package/package.json +1 -1
|
@@ -97,6 +97,19 @@ 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
|
+
deduplicated: {
|
|
103
|
+
knowledge: number;
|
|
104
|
+
lessons: number;
|
|
105
|
+
events: number;
|
|
106
|
+
preferences: number;
|
|
107
|
+
facets: number;
|
|
108
|
+
work: number;
|
|
109
|
+
total: number;
|
|
110
|
+
};
|
|
111
|
+
migratedEpisodesToV2: number;
|
|
112
|
+
}>;
|
|
100
113
|
private syncEpisodeToV2;
|
|
101
114
|
private mapEpisodeToV2;
|
|
102
115
|
private makeEpisodeMemoryId;
|
|
@@ -653,6 +653,43 @@ export class NanoMemEngine {
|
|
|
653
653
|
async getAllEpisodes() {
|
|
654
654
|
return loadEpisodes(this.episodesDir);
|
|
655
655
|
}
|
|
656
|
+
async runStartupMaintenance(maintenanceVersion = 1) {
|
|
657
|
+
const [meta, v2Meta] = await Promise.all([loadMeta(this.metaPath), loadV2Meta(this.v2Paths)]);
|
|
658
|
+
const alreadyMaintained = (meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion &&
|
|
659
|
+
(v2Meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion;
|
|
660
|
+
if (alreadyMaintained) {
|
|
661
|
+
return {
|
|
662
|
+
ran: false,
|
|
663
|
+
deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
|
|
664
|
+
migratedEpisodesToV2: 0,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const now = new Date().toISOString();
|
|
668
|
+
const deduplicated = await this.deduplicateAll();
|
|
669
|
+
const episodes = await this.getAllEpisodes();
|
|
670
|
+
for (const episode of episodes) {
|
|
671
|
+
await this.syncEpisodeToV2(episode);
|
|
672
|
+
}
|
|
673
|
+
await Promise.all([
|
|
674
|
+
writeJson(this.metaPath, {
|
|
675
|
+
...(await loadMeta(this.metaPath)),
|
|
676
|
+
lastMaintenanceAt: now,
|
|
677
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
678
|
+
}),
|
|
679
|
+
saveV2Meta(this.v2Paths, {
|
|
680
|
+
...(await loadV2Meta(this.v2Paths)),
|
|
681
|
+
version: 2,
|
|
682
|
+
lastMaintenanceAt: now,
|
|
683
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
684
|
+
lastMigrationAt: (await loadV2Meta(this.v2Paths)).lastMigrationAt ?? now,
|
|
685
|
+
}),
|
|
686
|
+
]);
|
|
687
|
+
return {
|
|
688
|
+
ran: true,
|
|
689
|
+
deduplicated,
|
|
690
|
+
migratedEpisodesToV2: episodes.length,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
656
693
|
async syncEpisodeToV2(ep) {
|
|
657
694
|
const [episodes, facets, links, procedural, meta] = await Promise.all([
|
|
658
695
|
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,24 @@ 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.deduplicated.total > 0) {
|
|
313
|
+
notes.push(`deduped ${maintenance.deduplicated.total} entries`);
|
|
314
|
+
}
|
|
315
|
+
if (maintenance.migratedEpisodesToV2 > 0) {
|
|
316
|
+
notes.push(`refreshed ${maintenance.migratedEpisodesToV2} episodes`);
|
|
317
|
+
}
|
|
318
|
+
if (notes.length > 0) {
|
|
319
|
+
ctx.ui.notify(`NanoMem maintenance completed: ${notes.join(", ")}`, "info");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error("[nanomem] startup maintenance failed:", error);
|
|
325
|
+
}
|
|
243
326
|
});
|
|
244
327
|
const maybeRunAutoDream = async (ctx) => {
|
|
245
328
|
const cfg = getDreamConfig(ctx);
|
|
@@ -310,18 +393,21 @@ export default function nanomemExtension(pi) {
|
|
|
310
393
|
if (sessionGoal === undefined && event.prompt?.trim())
|
|
311
394
|
sessionGoal = event.prompt.trim().slice(0, 300);
|
|
312
395
|
const cacheFresh = cachedInjection && Date.now() - lastInjectionAt < 30_000;
|
|
396
|
+
const recallInjection = await withTimeout(buildMemoryRecallInjection(engine, project, event.prompt ?? ""), 250);
|
|
313
397
|
if (cacheFresh) {
|
|
314
398
|
void refreshInjection();
|
|
315
|
-
|
|
399
|
+
const additions = [cachedInjection, recallInjection].filter(Boolean).join("\n\n");
|
|
400
|
+
return additions ? { systemPrompt: `${event.systemPrompt}\n\n${additions}` } : undefined;
|
|
316
401
|
}
|
|
317
402
|
const freshInjection = await withTimeout(engine.getMemoryInjection(project, ctxTags), 600);
|
|
318
403
|
if (freshInjection) {
|
|
319
404
|
cachedInjection = freshInjection;
|
|
320
405
|
lastInjectionAt = Date.now();
|
|
321
|
-
|
|
406
|
+
const additions = [freshInjection, recallInjection].filter(Boolean).join("\n\n");
|
|
407
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${additions}` };
|
|
322
408
|
}
|
|
323
409
|
void refreshInjection();
|
|
324
|
-
return undefined;
|
|
410
|
+
return recallInjection ? { systemPrompt: `${event.systemPrompt}\n\n${recallInjection}` } : undefined;
|
|
325
411
|
});
|
|
326
412
|
pi.on("tool_execution_start", async (event) => {
|
|
327
413
|
pendingArgs.set(event.toolCallId, event.args);
|
|
@@ -163,6 +163,8 @@ export interface V2Meta {
|
|
|
163
163
|
lastMigrationAt?: string;
|
|
164
164
|
lastEmbeddingSyncAt?: string;
|
|
165
165
|
lastReconsolidationAt?: string;
|
|
166
|
+
lastMaintenanceAt?: string;
|
|
167
|
+
lastMaintenanceVersion?: number;
|
|
166
168
|
}
|
|
167
169
|
export interface NanoMemV2Snapshot {
|
|
168
170
|
episodes: EpisodeMemory[];
|
|
@@ -114,6 +114,8 @@ export interface Meta {
|
|
|
114
114
|
totalSessions: number;
|
|
115
115
|
lastConsolidation?: string;
|
|
116
116
|
version: number;
|
|
117
|
+
lastMaintenanceAt?: string;
|
|
118
|
+
lastMaintenanceVersion?: number;
|
|
117
119
|
}
|
|
118
120
|
/** Mem0-style update operations */
|
|
119
121
|
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.20",
|
|
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": {
|