pi-hermes-memory 0.6.4 → 0.6.6

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.
@@ -0,0 +1,57 @@
1
+ # v0.6.6 Changelog
2
+
3
+ ## Bug Fixes
4
+
5
+ ### Fix legacy SQLite upgrade error: `no such column: category`
6
+
7
+ Some users upgrading from older versions had an existing `sessions.db` with a legacy
8
+ `memories` table that did not include v0.6 failure-memory columns. During schema init,
9
+ creating `idx_memories_category` failed and `/memory-index-sessions` crashed with:
10
+
11
+ `❌ Session indexing failed: no such column: category`
12
+
13
+ Fix:
14
+
15
+ - Added automatic legacy schema migration in `DatabaseManager`
16
+ - Detects missing `memories` columns and adds them idempotently:
17
+ - `category`
18
+ - `failure_reason`
19
+ - `tool_state`
20
+ - `corrected_to`
21
+ - Retries schema initialization after migration
22
+ - Added regression test: `should migrate legacy memories table without category column`
23
+
24
+ ---
25
+
26
+ # v0.6.5 Changelog
27
+
28
+ ## Bug Fixes
29
+
30
+ ### Auto-review no longer blocks interactive chat (#10)
31
+
32
+ The background auto-review was `await`ing `pi.exec()` inside the `turn_end` handler, which
33
+ blocked the chat from responding while the review subprocess ran. Fixed by making the
34
+ review subprocess fire-and-forget — the turn_end handler returns immediately, and the
35
+ subprocess completes asynchronously.
36
+
37
+ - `pi.exec()` is no longer awaited — handler returns immediately
38
+ - `reviewInProgress` guard resets in `.then()` / `.catch()` callbacks
39
+ - Overlapping reviews are still prevented by the guard
40
+ - Notifications are delivered via `.then()` callback once the subprocess completes
41
+
42
+ ### Auto-review failures on Windows no longer show errors (#9)
43
+
44
+ On Windows git-bash, `pi exec` subprocesses could exit with code 1 producing
45
+ `[hermes] auto-review failed (exit=1): unknown error` messages every few turns. Fixed by
46
+ making auto-review truly best-effort — non-zero exit codes and spawn errors are silently
47
+ ignored.
48
+
49
+ - Suppressed error notifications for non-zero exit codes
50
+ - Suppressed error notifications for subprocess failures (timeout, signal, spawn)
51
+ - The next review cycle will retry automatically
52
+
53
+ ### Crash safety
54
+
55
+ - Snapshot-building code (`getBranch()`, message parsing) is wrapped in a minimal try/catch
56
+ to handle expired sessions gracefully
57
+ - Early return resets `reviewInProgress` guard to unblock future reviews
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hermes-memory",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "🧠 Persistent memory + 🔍 session search + 🛡️ secret scanning for Pi. SQLite FTS5 search across every conversation, auto-consolidation, memory aging, procedural skills. 272 tests. Ported from Hermes agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -66,10 +66,10 @@ export function setupBackgroundReview(
66
66
  toolCallsSinceReview = 0;
67
67
  reviewInProgress = true;
68
68
 
69
+ // Build conversation snapshot from session entries (crash-safe)
70
+ let parts: string[] = [];
69
71
  try {
70
- // Build conversation snapshot from session entries
71
72
  const entries = ctx.sessionManager.getBranch();
72
- const parts: string[] = [];
73
73
 
74
74
  for (const entry of entries) {
75
75
  if (entry.type !== "message") continue;
@@ -79,56 +79,72 @@ export function setupBackgroundReview(
79
79
  const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
80
80
  parts.push(`${prefix}: ${text}`);
81
81
  }
82
- if (parts.length < 4) return; // Not enough conversation to review
83
-
84
- const currentMemory = store.getMemoryEntries().join("\n§\n");
85
- const currentUser = store.getUserEntries().join("\n§\n");
86
- const currentProject = projectStore ? projectStore.getMemoryEntries().join("\n§\n") : null;
87
-
88
- const reviewPrompt = [
89
- COMBINED_REVIEW_PROMPT,
90
- "",
91
- "--- Current Memory ---",
92
- currentMemory || "(empty)",
93
- "",
94
- "--- Current User Profile ---",
95
- currentUser || "(empty)",
96
- ];
97
-
98
- if (currentProject !== null) {
99
- reviewPrompt.push(
100
- "",
101
- "--- Current Project Memory ---",
102
- currentProject || "(empty)",
103
- );
104
- }
82
+ } catch {
83
+ reviewInProgress = false;
84
+ return; // Session expired or empty — nothing to review
85
+ }
86
+ if (parts.length < 4) {
87
+ reviewInProgress = false;
88
+ return; // Not enough conversation to review
89
+ }
105
90
 
91
+ const currentMemory = store.getMemoryEntries().join("\n§\n");
92
+ const currentUser = store.getUserEntries().join("\n§\n");
93
+ const currentProject = projectStore ? projectStore.getMemoryEntries().join("\n§\n") : null;
94
+
95
+ const reviewPrompt = [
96
+ COMBINED_REVIEW_PROMPT,
97
+ "",
98
+ "--- Current Memory ---",
99
+ currentMemory || "(empty)",
100
+ "",
101
+ "--- Current User Profile ---",
102
+ currentUser || "(empty)",
103
+ ];
104
+
105
+ if (currentProject !== null) {
106
106
  reviewPrompt.push(
107
107
  "",
108
- "--- Conversation to Review ---",
109
- parts.join("\n\n"),
108
+ "--- Current Project Memory ---",
109
+ currentProject || "(empty)",
110
110
  );
111
+ }
111
112
 
112
- const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt.join("\n")], {
113
- signal: ctx.signal,
114
- timeout: 120000,
115
- });
116
-
117
- if (result.code === 0 && result.stdout) {
118
- const output = result.stdout.trim();
119
- if (output && !output.toLowerCase().includes("nothing to save")) {
120
- ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
113
+ reviewPrompt.push(
114
+ "",
115
+ "--- Conversation to Review ---",
116
+ parts.join("\n\n"),
117
+ );
118
+
119
+ // Fire-and-forget: do NOT await. The review runs in a subprocess;
120
+ // blocking turn_end would freeze the interactive chat.
121
+ // Notifications are delivered via .then() once the subprocess completes.
122
+ //
123
+ // We intentionally omit ctx.signal — the signal is tied to the turn
124
+ // lifetime and would abort the subprocess before it finishes now that
125
+ // we're not awaiting. The timeout (120s) provides its own safety net.
126
+ const reviewPromise = pi.exec("pi", ["-p", "--no-session", reviewPrompt.join("\n")], {
127
+ signal: undefined,
128
+ timeout: 120000,
129
+ });
130
+
131
+ reviewPromise
132
+ .then((result) => {
133
+ reviewInProgress = false;
134
+ if (result.code === 0 && result.stdout) {
135
+ const output = result.stdout.trim();
136
+ if (output && !output.toLowerCase().includes("nothing to save")) {
137
+ ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
138
+ }
121
139
  }
122
- } else {
123
- ctx.ui.notify(
124
- `[hermes] auto-review failed (exit=${result.code}): ${result.stderr?.slice(0, 200) || "unknown error"}`,
125
- "error",
126
- );
127
- }
128
- } catch (err) {
129
- ctx.ui.notify(`[hermes] auto-review error: ${String(err).slice(0, 200)}`, "error");
130
- } finally {
131
- reviewInProgress = false;
132
- }
140
+ // Auto-review is best-effort. Non-zero exits are silently skipped —
141
+ // common on Windows where pi CLI may resolve differently. The next
142
+ // review cycle will retry.
143
+ })
144
+ .catch(() => {
145
+ // Best-effort: subprocess failures (timeout, signal, spawn errors)
146
+ // are silently ignored. The next review cycle will retry.
147
+ reviewInProgress = false;
148
+ });
133
149
  });
134
150
  }
package/src/store/db.ts CHANGED
@@ -38,11 +38,53 @@ export class DatabaseManager {
38
38
  db.pragma('foreign_keys = ON');
39
39
 
40
40
  // Create tables and triggers
41
- db.exec(SCHEMA_SQL);
41
+ try {
42
+ db.exec(SCHEMA_SQL);
43
+ } catch (err) {
44
+ if (!this.isLegacyMemoriesCategoryError(err)) {
45
+ throw err;
46
+ }
47
+
48
+ // Legacy DB from pre-v0.6 can have memories table without the category
49
+ // and failure metadata columns. Add missing columns, then retry schema.
50
+ this.ensureMemoriesColumns(db);
51
+ db.exec(SCHEMA_SQL);
52
+ }
53
+
54
+ // Extra safety: always ensure the legacy memories columns exist, even when
55
+ // schema execution succeeds (idempotent on upgraded DBs).
56
+ this.ensureMemoriesColumns(db);
42
57
 
43
58
  return db;
44
59
  }
45
60
 
61
+ private isLegacyMemoriesCategoryError(err: unknown): boolean {
62
+ if (!(err instanceof Error)) return false;
63
+ const msg = err.message.toLowerCase();
64
+ return msg.includes('no such column: category') || msg.includes('memories(category)');
65
+ }
66
+
67
+ private ensureMemoriesColumns(db: Database.Database): void {
68
+ const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'").get() as { name: string } | undefined;
69
+ if (!tableExists) return;
70
+
71
+ const columns = db.prepare('PRAGMA table_info(memories)').all() as { name: string }[];
72
+ const names = new Set(columns.map((c) => c.name));
73
+
74
+ if (!names.has('category')) {
75
+ db.exec('ALTER TABLE memories ADD COLUMN category TEXT');
76
+ }
77
+ if (!names.has('failure_reason')) {
78
+ db.exec('ALTER TABLE memories ADD COLUMN failure_reason TEXT');
79
+ }
80
+ if (!names.has('tool_state')) {
81
+ db.exec('ALTER TABLE memories ADD COLUMN tool_state TEXT');
82
+ }
83
+ if (!names.has('corrected_to')) {
84
+ db.exec('ALTER TABLE memories ADD COLUMN corrected_to TEXT');
85
+ }
86
+ }
87
+
46
88
  /**
47
89
  * Close the database connection.
48
90
  */