@pencil-agent/nano-pencil 1.11.20 → 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.
@@ -99,6 +99,7 @@ export declare class NanoMemEngine {
99
99
  getAllEpisodes(): Promise<Episode[]>;
100
100
  runStartupMaintenance(maintenanceVersion?: number): Promise<{
101
101
  ran: boolean;
102
+ backupPath?: string;
102
103
  deduplicated: {
103
104
  knowledge: number;
104
105
  lessons: number;
@@ -110,6 +111,7 @@ export declare class NanoMemEngine {
110
111
  };
111
112
  migratedEpisodesToV2: number;
112
113
  }>;
114
+ private createMaintenanceBackup;
113
115
  private syncEpisodeToV2;
114
116
  private mapEpisodeToV2;
115
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";
@@ -660,11 +661,13 @@ export class NanoMemEngine {
660
661
  if (alreadyMaintained) {
661
662
  return {
662
663
  ran: false,
664
+ backupPath: undefined,
663
665
  deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
664
666
  migratedEpisodesToV2: 0,
665
667
  };
666
668
  }
667
669
  const now = new Date().toISOString();
670
+ const backupPath = await this.createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now);
668
671
  const deduplicated = await this.deduplicateAll();
669
672
  const episodes = await this.getAllEpisodes();
670
673
  for (const episode of episodes) {
@@ -675,6 +678,8 @@ export class NanoMemEngine {
675
678
  ...(await loadMeta(this.metaPath)),
676
679
  lastMaintenanceAt: now,
677
680
  lastMaintenanceVersion: maintenanceVersion,
681
+ lastBackupAt: meta.lastBackupAt ?? now,
682
+ lastBackupVersion: Math.max(meta.lastBackupVersion ?? 0, maintenanceVersion),
678
683
  }),
679
684
  saveV2Meta(this.v2Paths, {
680
685
  ...(await loadV2Meta(this.v2Paths)),
@@ -682,14 +687,60 @@ export class NanoMemEngine {
682
687
  lastMaintenanceAt: now,
683
688
  lastMaintenanceVersion: maintenanceVersion,
684
689
  lastMigrationAt: (await loadV2Meta(this.v2Paths)).lastMigrationAt ?? now,
690
+ lastBackupAt: v2Meta.lastBackupAt ?? now,
691
+ lastBackupVersion: Math.max(v2Meta.lastBackupVersion ?? 0, maintenanceVersion),
685
692
  }),
686
693
  ]);
687
694
  return {
688
695
  ran: true,
696
+ backupPath,
689
697
  deduplicated,
690
698
  migratedEpisodesToV2: episodes.length,
691
699
  };
692
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
+ }
693
744
  async syncEpisodeToV2(ep) {
694
745
  const [episodes, facets, links, procedural, meta] = await Promise.all([
695
746
  loadV2Episodes(this.v2Paths),
@@ -309,6 +309,9 @@ export default function nanomemExtension(pi) {
309
309
  const maintenance = await engine.runStartupMaintenance(1);
310
310
  if (maintenance.ran && ctx.hasUI) {
311
311
  const notes = [];
312
+ if (maintenance.backupPath) {
313
+ notes.push(`backup saved to ${maintenance.backupPath}`);
314
+ }
312
315
  if (maintenance.deduplicated.total > 0) {
313
316
  notes.push(`deduped ${maintenance.deduplicated.total} entries`);
314
317
  }
@@ -165,6 +165,8 @@ export interface V2Meta {
165
165
  lastReconsolidationAt?: string;
166
166
  lastMaintenanceAt?: string;
167
167
  lastMaintenanceVersion?: number;
168
+ lastBackupAt?: string;
169
+ lastBackupVersion?: number;
168
170
  }
169
171
  export interface NanoMemV2Snapshot {
170
172
  episodes: EpisodeMemory[];
@@ -116,6 +116,8 @@ export interface Meta {
116
116
  version: number;
117
117
  lastMaintenanceAt?: string;
118
118
  lastMaintenanceVersion?: number;
119
+ lastBackupAt?: string;
120
+ lastBackupVersion?: number;
119
121
  }
120
122
  /** Mem0-style update operations */
121
123
  export type UpdateAction = "add" | "update" | "delete" | "noop";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.11.20",
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": {