@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.
@@ -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
- return { systemPrompt: `${event.systemPrompt}\n\n${cachedInjection}` };
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
- return { systemPrompt: `${event.systemPrompt}\n\n${freshInjection}` };
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're still getting to know the user. Pay attention to their preferences.";
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(`You've worked with this user ${relationship.interactionCount} times.`);
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("Maintain a professional, formal tone.");
226
+ context.push("Keep the tone professional and composed.");
228
227
  break;
229
228
  case "casual":
230
- context.push("Keep communication friendly and casual.");
229
+ context.push("Keep the tone relaxed and friendly.");
231
230
  break;
232
231
  case "technical":
233
- context.push("Use technical language and dive deep into details.");
232
+ context.push("Lean technical when useful and go deeper on details.");
234
233
  break;
235
234
  case "mixed":
236
- context.push("Adapt your communication style to the context.");
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
- // Header
250
- parts.push("## Your Soul");
251
- parts.push("");
252
- parts.push("_Your personality and experiences shape how you approach tasks._");
253
- parts.push("");
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
- parts.push("### Personality Traits");
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
- parts.push("### Values");
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
- parts.push("### Thinking Style");
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("### Expertise");
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("### Current Mood");
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("### Relationship with User");
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.19",
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": {