@pi-unipi/memory 2.1.0 → 2.1.1

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.
Files changed (3) hide show
  1. package/mempalace.ts +28 -0
  2. package/package.json +3 -3
  3. package/storage.ts +37 -21
package/mempalace.ts CHANGED
@@ -22,6 +22,10 @@ export const DEFAULT_PALACE = path.join(os.homedir(), ".mempalace", "palace");
22
22
 
23
23
  const INSTALL_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-install");
24
24
  const MIGRATED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-migrated");
25
+ /** Flag written after a successful ping, so subsequent sessions can skip
26
+ * the ~0.5s Python cold-start sanity check. Stale after PING_VERIFIED_TTL_MS. */
27
+ const PING_VERIFIED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
28
+ const PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1000; // 24h
25
29
 
26
30
  /** Path to the bundled bridge script. */
27
31
  const BRIDGE_PATH = path.join(dirname(fileURLToPath(import.meta.url)), "bridge", "mempalace_bridge.py");
@@ -166,6 +170,30 @@ export function ensureMempalace(): MempalaceInstall | null {
166
170
  /** Drop the cached install record (forces re-detection next session). */
167
171
  export function invalidateInstallCache(): void {
168
172
  try { if (fs.existsSync(INSTALL_FLAG)) fs.unlinkSync(INSTALL_FLAG); } catch { /* ignore */ }
173
+ invalidatePingVerified();
174
+ }
175
+
176
+ /** Was the palace ping-verified recently enough to trust without re-pinging? */
177
+ export function isPingVerified(): boolean {
178
+ try {
179
+ if (!fs.existsSync(PING_VERIFIED_FLAG)) return false;
180
+ const ts = Number.parseInt(fs.readFileSync(PING_VERIFIED_FLAG, "utf-8").trim(), 10);
181
+ if (!Number.isFinite(ts)) return false;
182
+ return Date.now() - ts < PING_VERIFIED_TTL_MS;
183
+ } catch { return false; }
184
+ }
185
+
186
+ /** Mark the palace as ping-verified (written after a successful ping). */
187
+ export function markPingVerified(): void {
188
+ try {
189
+ fs.mkdirSync(path.dirname(PING_VERIFIED_FLAG), { recursive: true });
190
+ fs.writeFileSync(PING_VERIFIED_FLAG, String(Date.now()), "utf-8");
191
+ } catch { /* ignore */ }
192
+ }
193
+
194
+ /** Drop the ping-verified flag (forces a real ping next session). */
195
+ export function invalidatePingVerified(): void {
196
+ try { if (fs.existsSync(PING_VERIFIED_FLAG)) fs.unlinkSync(PING_VERIFIED_FLAG); } catch { /* ignore */ }
169
197
  }
170
198
 
171
199
  /** Has the one-way legacy migration been completed? */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -43,8 +43,8 @@
43
43
  "better-sqlite3": "^12.9.0",
44
44
  "sqlite-vec": "^0.1.9",
45
45
  "js-yaml": "^4.1.0",
46
- "@pi-unipi/core": "2.1.0",
47
- "@pi-unipi/info-screen": "2.1.0"
46
+ "@pi-unipi/core": "2.1.1",
47
+ "@pi-unipi/info-screen": "2.1.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-coding-agent": "^0.80.0",
package/storage.ts CHANGED
@@ -19,6 +19,9 @@ import {
19
19
  runBridge,
20
20
  isMigrated,
21
21
  markMigrated,
22
+ isPingVerified,
23
+ markPingVerified,
24
+ invalidatePingVerified,
22
25
  DEFAULT_PALACE,
23
26
  type MempalaceInstall,
24
27
  type MempalaceRecord,
@@ -262,6 +265,22 @@ export class MemoryStorage {
262
265
  return this.backend === "mempalace" && this.mempalaceInstall !== null;
263
266
  }
264
267
 
268
+ /**
269
+ * Run a MemPalace bridge command, invalidating the ping-verified flag
270
+ * when the call fails. This ensures a palace that breaks after the
271
+ * startup ping-skip gets re-verified on the next session.
272
+ */
273
+ private memPalaceCall<T>(cmd: string, args: Record<string, unknown> = {}): T | null {
274
+ const install = this.mempalaceInstall;
275
+ if (!install) return null;
276
+ const result = runBridge<T>(install, this.palacePath, cmd, args);
277
+ if (result === null) {
278
+ // Backend didn't respond — force a real ping next session.
279
+ invalidatePingVerified();
280
+ }
281
+ return result;
282
+ }
283
+
265
284
  /**
266
285
  * Initialize storage. Tries MemPalace first (auto-install + one-way
267
286
  * auto-migration of legacy memories); falls back to SQLite if MemPalace
@@ -325,8 +344,14 @@ export class MemoryStorage {
325
344
  if (!install) return false;
326
345
 
327
346
  // Sanity ping — if the palace/bridge is broken, fall back.
328
- const ok = runBridge<string>(install, this.palacePath, "ping");
329
- if (ok !== "pong") return false;
347
+ // Skip the ~0.5s Python cold-start when we ping-verified recently;
348
+ // the flag is invalidated on any backend failure so a broken palace
349
+ // is re-checked on the next session.
350
+ if (!isPingVerified()) {
351
+ const ok = runBridge<string>(install, this.palacePath, "ping");
352
+ if (ok !== "pong") return false;
353
+ markPingVerified();
354
+ }
330
355
 
331
356
  this.mempalaceInstall = install;
332
357
  this.backend = "mempalace";
@@ -539,8 +564,7 @@ export class MemoryStorage {
539
564
  * consistent and durable as a fallback source.
540
565
  */
541
566
  private storeMempalace(record: MemoryRecord): void {
542
- const install = this.mempalaceInstall!;
543
- runBridge(install, this.palacePath, "store", {
567
+ this.memPalaceCall("store", {
544
568
  record: {
545
569
  id: record.id,
546
570
  title: record.title,
@@ -572,8 +596,7 @@ export class MemoryStorage {
572
596
  */
573
597
  syncOrphanedFiles(): number {
574
598
  if (this.isMempalace()) {
575
- const install = this.mempalaceInstall!;
576
- const synced = runBridge<number>(install, this.palacePath, "sync_orphaned", {
599
+ const synced = this.memPalaceCall<number>("sync_orphaned", {
577
600
  project_dir: this.scopeDir,
578
601
  wing: this.projectName,
579
602
  });
@@ -636,8 +659,7 @@ export class MemoryStorage {
636
659
  */
637
660
  hasByTitle(title: string): boolean {
638
661
  if (this.isMempalace()) {
639
- const install = this.mempalaceInstall!;
640
- return runBridge<boolean>(install, this.palacePath, "has_title", {
662
+ return this.memPalaceCall<boolean>("has_title", {
641
663
  wing: this.projectName,
642
664
  title,
643
665
  }) ?? false;
@@ -654,9 +676,8 @@ export class MemoryStorage {
654
676
  */
655
677
  findSimilarByTitle(title: string, threshold = 0.6): Array<{ record: MemoryRecord; similarity: number }> {
656
678
  if (this.isMempalace()) {
657
- const install = this.mempalaceInstall!;
658
- const rows = runBridge<Array<{ record: MempalaceRecord; similarity: number }>>(
659
- install, this.palacePath, "find_similar",
679
+ const rows = this.memPalaceCall<Array<{ record: MempalaceRecord; similarity: number }>>(
680
+ "find_similar",
660
681
  { wing: this.projectName, title, threshold },
661
682
  ) ?? [];
662
683
  return rows.map((r) => ({ record: toMemoryRecord(r.record), similarity: r.similarity }));
@@ -694,8 +715,7 @@ export class MemoryStorage {
694
715
  */
695
716
  getById(id: string): MemoryRecord | null {
696
717
  if (this.isMempalace()) {
697
- const install = this.mempalaceInstall!;
698
- const rec = runBridge<MempalaceRecord | null>(install, this.palacePath, "get", { id });
718
+ const rec = this.memPalaceCall<MempalaceRecord | null>("get", { id });
699
719
  return rec ? toMemoryRecord(rec) : null;
700
720
  }
701
721
  if (!this.db) throw new Error("Storage not initialized");
@@ -721,8 +741,7 @@ export class MemoryStorage {
721
741
  */
722
742
  getByTitle(title: string): MemoryRecord | null {
723
743
  if (this.isMempalace()) {
724
- const install = this.mempalaceInstall!;
725
- const rec = runBridge<MempalaceRecord | null>(install, this.palacePath, "get_by_title", {
744
+ const rec = this.memPalaceCall<MempalaceRecord | null>("get_by_title", {
726
745
  wing: this.projectName,
727
746
  title,
728
747
  });
@@ -768,8 +787,7 @@ export class MemoryStorage {
768
787
  */
769
788
  listAll(): Array<{ id: string; title: string; type: string }> {
770
789
  if (this.isMempalace()) {
771
- const install = this.mempalaceInstall!;
772
- const items = runBridge<MempalaceListItem[]>(install, this.palacePath, "list", {
790
+ const items = this.memPalaceCall<MempalaceListItem[]>("list", {
773
791
  wing: this.projectName,
774
792
  }) ?? [];
775
793
  return items;
@@ -785,8 +803,7 @@ export class MemoryStorage {
785
803
  */
786
804
  delete(id: string): boolean {
787
805
  if (this.isMempalace()) {
788
- const install = this.mempalaceInstall!;
789
- const ok = runBridge<boolean>(install, this.palacePath, "delete", { id }) ?? false;
806
+ const ok = this.memPalaceCall<boolean>("delete", { id }) ?? false;
790
807
  // Also remove the markdown tier if present.
791
808
  try {
792
809
  const mdPath = path.join(this.scopeDir, `${id}.md`);
@@ -833,8 +850,7 @@ export class MemoryStorage {
833
850
  */
834
851
  search(query: string, limit = 10, embedding?: Float32Array | null): SearchResult[] {
835
852
  if (this.isMempalace()) {
836
- const install = this.mempalaceInstall!;
837
- const rows = runBridge<MempalaceSearchResult[]>(install, this.palacePath, "search", {
853
+ const rows = this.memPalaceCall<MempalaceSearchResult[]>("search", {
838
854
  query,
839
855
  wing: this.projectName,
840
856
  limit,