pi-hermes-memory 0.1.0

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,272 @@
1
+ # Pi Hermes Memory — Roadmap
2
+
3
+ > From markdown files to a pluggable memory substrate for any Pi agent harness.
4
+
5
+ ## Where We Are (v0.1.0)
6
+
7
+ - Persistent memory via `MEMORY.md` + `USER.md` with `§` delimiter
8
+ - Real-time `memory` tool (add / replace / remove) for the LLM
9
+ - Content scanning: prompt injection, role hijacking, secret exfiltration, invisible unicode
10
+ - Background learning loop (every N turns via `pi.exec`)
11
+ - Session flush before compaction and shutdown
12
+ - `/memory-insights` command
13
+ - Frozen snapshot injection into system prompt
14
+ - 119 automated tests, 0 type errors
15
+ - Atomic writes (temp + rename)
16
+
17
+ ## Architecture Evolution
18
+
19
+ ```mermaid
20
+ graph TB
21
+ subgraph "v0.1.0 — Current"
22
+ T1["memory tool<br/>(add / replace / remove)"]
23
+ SC["Content Scanner<br/>(injection · exfiltration · unicode)"]
24
+ MD["Markdown Backend<br/>MEMORY.md · USER.md"]
25
+ FS["Frozen Snapshot<br/>(system prompt injection)"]
26
+ BL["Background Review<br/>(pi.exec child process)"]
27
+ SF["Session Flush<br/>(compact · shutdown)"]
28
+ IC["/memory-insights<br/>(command)"]
29
+ CF["Config File<br/>(hermes-memory-config.json)"]
30
+ end
31
+
32
+ T1 --> SC --> MD
33
+ BL --> MD
34
+ SF --> MD
35
+ MD --> FS
36
+
37
+ style T1 fill:#e94560,stroke:#fff,color:#fff
38
+ style SC fill:#ff6600,stroke:#fff,color:#fff
39
+ style MD fill:#0f3460,stroke:#fff,color:#fff
40
+ style FS fill:#16213e,stroke:#fff,color:#fff
41
+ style BL fill:#16213e,stroke:#fff,color:#fff
42
+ style SF fill:#16213e,stroke:#fff,color:#fff
43
+ style IC fill:#16213e,stroke:#fff,color:#fff
44
+ style CF fill:#16213e,stroke:#fff,color:#fff
45
+ ```
46
+
47
+ ```mermaid
48
+ graph TB
49
+ subgraph "v0.2.0 — Structured Storage & Search"
50
+ T2["memory tool<br/>(add / replace / remove / search)"]
51
+ SC2["Content Scanner<br/>(v0.1.0 scanner unchanged)"]
52
+ SA["Search Abstraction<br/>(MemoryBackend interface)"]
53
+ SQL["SQLite Backend<br/>(FTS5 · key-value · confidence)"]
54
+ PI2["Context-Aware Injection<br/>(relevance-filtered)"]
55
+ PS["Project-Scoped Memory<br/>(keyed by cwd)"]
56
+ end
57
+
58
+ T2 --> SC2 --> SA
59
+ SA --> SQL
60
+ SQL --> PI2
61
+ SQL --> PS
62
+
63
+ style T2 fill:#e94560,stroke:#fff,color:#fff
64
+ style SC2 fill:#ff6600,stroke:#fff,color:#fff
65
+ style SA fill:#1282a2,stroke:#fff,color:#fff
66
+ style SQL fill:#0f3460,stroke:#fff,color:#fff
67
+ style PI2 fill:#16213e,stroke:#fff,color:#fff
68
+ style PS fill:#16213e,stroke:#fff,color:#fff
69
+ ```
70
+
71
+ ```mermaid
72
+ graph TB
73
+ subgraph "v0.3.0 — Pluggable Backend & External Memory"
74
+ T3["memory tool<br/>(add / replace / remove / search)"]
75
+ SC3["Content Scanner<br/>(unchanged — guards all backends)"]
76
+ SA3["Search Abstraction<br/>(MemoryBackend interface)"]
77
+ LOC["Local SQLite<br/>(default · offline)"]
78
+ M0["Mem0 Backend<br/>(vector search · cloud)"]
79
+ HON["Honcho Backend<br/>(dialectic reasoning · Hermes-native)"]
80
+ SEL["Selective Injection<br/>(search-relevant · project-scoped)"]
81
+ end
82
+
83
+ T3 --> SC3 --> SA3
84
+ SA3 --> LOC
85
+ SA3 --> M0
86
+ SA3 --> HON
87
+ LOC --> SEL
88
+ M0 --> SEL
89
+ HON --> SEL
90
+
91
+ style T3 fill:#e94560,stroke:#fff,color:#fff
92
+ style SC3 fill:#ff6600,stroke:#fff,color:#fff
93
+ style SA3 fill:#1282a2,stroke:#fff,color:#fff
94
+ style LOC fill:#0f3460,stroke:#fff,color:#fff
95
+ style M0 fill:#6b21a8,stroke:#fff,color:#fff
96
+ style HON fill:#6b21a8,stroke:#fff,color:#fff
97
+ style SEL fill:#16213e,stroke:#fff,color:#fff
98
+ ```
99
+
100
+ ```mermaid
101
+ graph TB
102
+ subgraph "v1.0.0 — Production Memory Substrate"
103
+ T4["memory tool<br/>(add / replace / remove / search / consolidate)"]
104
+ SC4["Content Scanner<br/>(extensible rule system)"]
105
+ SA4["Pluggable Backend<br/>(local · Mem0 · Honcho · custom)"]
106
+ CON["Smart Consolidation<br/>(structured extraction · dedup)"]
107
+ MUL["Multi-Agent Memory<br/>(shared context · scoping)"]
108
+ OBS["Observability<br/>(memory stats · usage · audit log)"]
109
+ end
110
+
111
+ T4 --> SC4 --> SA4
112
+ SA4 --> CON
113
+ SA4 --> MUL
114
+ CON --> OBS
115
+
116
+ style T4 fill:#e94560,stroke:#fff,color:#fff
117
+ style SC4 fill:#ff6600,stroke:#fff,color:#fff
118
+ style SA4 fill:#1282a2,stroke:#fff,color:#fff
119
+ style CON fill:#16213e,stroke:#fff,color:#fff
120
+ style MUL fill:#16213e,stroke:#fff,color:#fff
121
+ style OBS fill:#16213e,stroke:#fff,color:#fff
122
+ ```
123
+
124
+ ---
125
+
126
+ ## v0.2.0 — Structured Storage & Search
127
+
128
+ **Goal**: Replace flat markdown with SQLite. Add search. Keep the same tool interface.
129
+
130
+ ### `MemoryBackend` Interface
131
+
132
+ The core abstraction that makes everything after this possible:
133
+
134
+ ```typescript
135
+ interface MemoryBackend {
136
+ // Write
137
+ add(target: "memory" | "user", entry: MemoryEntry): Promise<MemoryResult>;
138
+ replace(target: "memory" | "user", query: string, entry: MemoryEntry): Promise<MemoryResult>;
139
+ remove(target: "memory" | "user", query: string): Promise<MemoryResult>;
140
+
141
+ // Read
142
+ getAll(target: "memory" | "user"): Promise<MemoryEntry[]>;
143
+ search(query: string, limit?: number): Promise<MemoryEntry[]>;
144
+
145
+ // Lifecycle
146
+ formatForSystemPrompt(cwd?: string, prompt?: string): Promise<string>;
147
+ close(): Promise<void>;
148
+ }
149
+ ```
150
+
151
+ Current `MemoryStore` becomes `MarkdownBackend` — the default, zero-dependency implementation. New `SQLiteBackend` adds structure without breaking anything.
152
+
153
+ ### Deliverables
154
+
155
+ - [ ] `MemoryBackend` interface in `src/types.ts`
156
+ - [ ] `MarkdownBackend` — wraps current `MemoryStore` (backwards compatible)
157
+ - [ ] `SQLiteBackend` — FTS5 search, key-value entries, confidence scores, dedup by key
158
+ - [ ] `memory search` tool action — LLM can query existing entries
159
+ - [ ] Project-scoped memory — entries tagged with `cwd`, injected when matching
160
+ - [ ] Context-aware injection — `formatForSystemPrompt(cwd, prompt)` filters by relevance
161
+ - [ ] Config: `"backend": "markdown" | "sqlite"` (defaults to `markdown` for zero-dep install)
162
+ - [ ] Migration tool: `markdown → sqlite` one-time import
163
+
164
+ ### What Does NOT Change
165
+
166
+ - Content scanner (guards all backends)
167
+ - Tool interface (`memory` tool name and actions)
168
+ - System prompt injection (frozen snapshot pattern)
169
+ - Config file location and format (just adds new fields)
170
+
171
+ ---
172
+
173
+ ## v0.3.0 — Pluggable External Memory
174
+
175
+ **Goal**: Let users swap the backend to Mem0 or Honcho without changing anything else. The content scanner guards all data before it leaves the machine.
176
+
177
+ ### Why This Matters
178
+
179
+ External memory services provide better semantic search, cross-session continuity, and multi-agent awareness. But they introduce trust boundaries — your agent's memories leave your machine. The content scanner becomes the security gate between Pi and any external service.
180
+
181
+ ### Deliverables
182
+
183
+ - [ ] `Mem0Backend` — wraps Mem0's Node.js SDK (`add`, `search`, `update`, `delete`)
184
+ - [ ] `HonchoBackend` — wraps Honcho's API (`honcho_context`, `honcho_search_conclusions`, `honcho_reasoning`)
185
+ - [ ] Backend auto-detection — check for `MEM0_API_KEY` or `HONCHO_API_KEY` env vars, offer to configure
186
+ - [ ] Config: `"backend": "sqlite" | "mem0" | "honcho"` with `"mem0": { "apiKey": "...", "orgId": "..." }` options
187
+ - [ ] Selective injection by default when using external backends (leverage their search APIs)
188
+ - [ ] Offline fallback — if external backend is unreachable, fall back to local SQLite cache
189
+ - [ ] Data export — `memory export` command to dump all entries as JSON
190
+
191
+ ### Security Model
192
+
193
+ ```
194
+ LLM tool call
195
+
196
+ Content Scanner (local, always runs first)
197
+ ↓ blocked? → return error to LLM
198
+ ↓ passed
199
+ MemoryBackend.add()
200
+
201
+ Mem0 / Honcho / SQLite / Markdown
202
+ ```
203
+
204
+ The scanner runs **before** any backend. No adversarial content reaches external services.
205
+
206
+ ---
207
+
208
+ ## v1.0.0 — Production Memory Substrate
209
+
210
+ **Goal**: The memory layer that any Pi agent harness can build on top of.
211
+
212
+ ### Deliverables
213
+
214
+ - [ ] Smart consolidation — structured extraction with typed output (preferences, patterns, corrections, tool prefs)
215
+ - [ ] Confidence scoring — entries gain confidence over time as they're referenced, decay if never used
216
+ - [ ] Multi-agent memory — shared context between agents, scoping rules (per-user, per-project, global)
217
+ - [ ] Extensible scanner rules — users can add custom patterns to the content scanner
218
+ - [ ] `/memory-insights` upgrade — show backend type, entry count, storage stats, last sync time
219
+ - [ ] Audit log — track all memory operations with timestamps (already in SQLite schema for `SQLiteBackend`)
220
+ - [ ] Import/export — migrate between backends without data loss
221
+ - [ ] Benchmarks — context injection latency, search relevance, token budget utilization
222
+
223
+ ---
224
+
225
+ ## Design Principles (Unchanging)
226
+
227
+ These hold across all versions:
228
+
229
+ 1. **Security first** — Content scanning before any write, regardless of backend. No exceptions.
230
+ 2. **Real-time saves** — The LLM can save memories mid-conversation via tool calls, not just at session end.
231
+ 3. **Frozen snapshot** — Memory is injected into the system prompt once at session start. Never mutated mid-session.
232
+ 4. **Crash safety** — Atomic writes for markdown, WAL mode for SQLite, graceful degradation for external backends.
233
+ 5. **Zero-config start** — Install and it works with sensible defaults. Configuration is for power users.
234
+ 6. **Backwards compatible** — Every new version is a drop-in upgrade. No breaking changes to the tool interface or config format without a major version bump.
235
+
236
+ ---
237
+
238
+ ## Version Timeline
239
+
240
+ ```mermaid
241
+ gantt
242
+ title Pi Hermes Memory — Release Timeline
243
+ dateFormat YYYY-MM-DD
244
+ axisFormat %b %Y
245
+
246
+ section v0.1.0
247
+ Core memory + scanner + tool + review + flush :done, v01, 2025-04-20, 5d
248
+
249
+ section v0.2.0
250
+ MemoryBackend interface :v02a, after v01, 7d
251
+ SQLite backend + FTS5 search :v02b, after v02a, 7d
252
+ memory search tool + project scoping :v02c, after v02b, 5d
253
+ Context-aware injection :v02d, after v02c, 5d
254
+
255
+ section v0.3.0
256
+ Mem0 backend :v03a, after v02d, 7d
257
+ Honcho backend :v03b, after v03a, 7d
258
+ Offline fallback + data export :v03c, after v03b, 5d
259
+
260
+ section v1.0.0
261
+ Smart consolidation + confidence :v1a, after v03c, 10d
262
+ Multi-agent memory + audit log :v1b, after v1a, 10d
263
+ Extensible scanner + benchmarks :v1c, after v1b, 7d
264
+ ```
265
+
266
+ ---
267
+
268
+ ## How to Contribute
269
+
270
+ See [TASKS.md](0.1/TASKS.md) for current work. Pick an unchecked item, mark it `[~]`, implement, mark it `[x]` with the commit hash.
271
+
272
+ For roadmap items, open an issue with the version tag (e.g. `v0.2.0`) and describe what you want to work on.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "pi-hermes-memory",
3
+ "version": "0.1.0",
4
+ "description": "Persistent memory and self-directed learning loop for Pi — ported from the Hermes agent harness. Security-first content scanning, real-time saves, and frozen snapshot injection.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE",
11
+ "docs"
12
+ ],
13
+ "pi": {
14
+ "extensions": [
15
+ "./src/index.ts"
16
+ ]
17
+ },
18
+ "scripts": {
19
+ "check": "tsc --noEmit",
20
+ "test": "npx tsx --test 'tests/**/*.test.ts' --test-concurrency=1"
21
+ },
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi-extension",
25
+ "memory",
26
+ "learning-loop",
27
+ "agent",
28
+ "hermes",
29
+ "persistent-memory",
30
+ "content-scanner"
31
+ ],
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/chandra447/pi-hermes-memory"
36
+ },
37
+ "peerDependencies": {
38
+ "@mariozechner/pi-coding-agent": "*"
39
+ },
40
+ "devDependencies": {
41
+ "@mariozechner/pi-ai": "^0.70.0",
42
+ "@mariozechner/pi-coding-agent": "^0.70.0",
43
+ "typebox": "^1.1.33",
44
+ "typescript": "^6.0.3"
45
+ }
46
+ }
package/src/config.ts ADDED
@@ -0,0 +1,49 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import type { MemoryConfig } from "./types.js";
5
+ import {
6
+ DEFAULT_MEMORY_CHAR_LIMIT,
7
+ DEFAULT_USER_CHAR_LIMIT,
8
+ DEFAULT_NUDGE_INTERVAL,
9
+ DEFAULT_FLUSH_MIN_TURNS,
10
+ } from "./constants.js";
11
+
12
+ const DEFAULT_CONFIG: MemoryConfig = {
13
+ memoryCharLimit: DEFAULT_MEMORY_CHAR_LIMIT,
14
+ userCharLimit: DEFAULT_USER_CHAR_LIMIT,
15
+ nudgeInterval: DEFAULT_NUDGE_INTERVAL,
16
+ reviewEnabled: true,
17
+ flushOnCompact: true,
18
+ flushOnShutdown: true,
19
+ flushMinTurns: DEFAULT_FLUSH_MIN_TURNS,
20
+ };
21
+
22
+ export const DEFAULT_CONFIG_PATH = path.join(
23
+ os.homedir(),
24
+ ".pi",
25
+ "agent",
26
+ "hermes-memory-config.json",
27
+ );
28
+
29
+ export function loadConfig(): MemoryConfig {
30
+ try {
31
+ if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
32
+ const raw = fs.readFileSync(DEFAULT_CONFIG_PATH, "utf-8");
33
+ const parsed = JSON.parse(raw);
34
+ // Merge: override defaults with user config
35
+ const config: MemoryConfig = { ...DEFAULT_CONFIG };
36
+ if (typeof parsed.memoryCharLimit === "number") config.memoryCharLimit = parsed.memoryCharLimit;
37
+ if (typeof parsed.userCharLimit === "number") config.userCharLimit = parsed.userCharLimit;
38
+ if (typeof parsed.nudgeInterval === "number") config.nudgeInterval = parsed.nudgeInterval;
39
+ if (typeof parsed.reviewEnabled === "boolean") config.reviewEnabled = parsed.reviewEnabled;
40
+ if (typeof parsed.flushOnCompact === "boolean") config.flushOnCompact = parsed.flushOnCompact;
41
+ if (typeof parsed.flushOnShutdown === "boolean") config.flushOnShutdown = parsed.flushOnShutdown;
42
+ if (typeof parsed.flushMinTurns === "number") config.flushMinTurns = parsed.flushMinTurns;
43
+ return config;
44
+ }
45
+ } catch {
46
+ // Fall back to defaults on parse error or access issues
47
+ }
48
+ return { ...DEFAULT_CONFIG };
49
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Constants — prompts, defaults, and delimiter.
3
+ * Ported from hermes-agent/tools/memory_tool.py and hermes-agent/run_agent.py.
4
+ * See PLAN.md → "Hermes Source File Reference Map" for exact source lines.
5
+ */
6
+
7
+ // ─── Entry delimiter (same as Hermes) ───
8
+ export const ENTRY_DELIMITER = "\n§\n";
9
+
10
+ // ─── Character limits (not tokens — model-independent) ───
11
+ export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
12
+ export const DEFAULT_USER_CHAR_LIMIT = 1375;
13
+
14
+ // ─── Learning loop defaults ───
15
+ export const DEFAULT_NUDGE_INTERVAL = 10;
16
+ export const DEFAULT_FLUSH_MIN_TURNS = 6;
17
+
18
+ // ─── File names ───
19
+ export const MEMORY_FILE = "MEMORY.md";
20
+ export const USER_FILE = "USER.md";
21
+
22
+ // ─── Tool description (ported from MEMORY_SCHEMA in hermes-agent/tools/memory_tool.py) ───
23
+ export const MEMORY_TOOL_DESCRIPTION = `Save durable information to persistent memory that survives across sessions. Memory is injected into future turns, so keep it compact and focused on facts that will still matter later.
24
+
25
+ WHEN TO SAVE (do this proactively, don't wait to be asked):
26
+ - User corrects you or says 'remember this' / 'don't do that again'
27
+ - User shares a preference, habit, or personal detail (name, role, timezone, coding style)
28
+ - You discover something about the environment (OS, installed tools, project structure)
29
+ - You learn a convention, API quirk, or workflow specific to this user's setup
30
+ - You identify a stable fact that will be useful again in future sessions
31
+
32
+ PRIORITY: User preferences and corrections > environment facts > procedural knowledge.
33
+
34
+ Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state.
35
+
36
+ TWO TARGETS:
37
+ - 'user': who the user is -- name, role, preferences, communication style, pet peeves
38
+ - 'memory': your notes -- environment facts, project conventions, tool quirks, lessons learned
39
+
40
+ ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).`;
41
+
42
+ // ─── Background review prompt (ported from _COMBINED_REVIEW_PROMPT in run_agent.py ~L2855) ───
43
+ export const COMBINED_REVIEW_PROMPT = `Review the conversation above and consider two things:
44
+
45
+ **Memory**: Has the user revealed things about themselves — their persona, desires, preferences, or personal details? Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate? If so, save using the memory tool.
46
+
47
+ **Skills**: Was a non-trivial approach used to complete a task that required trial and error, or changing course due to experiential findings along the way, or did the user expect or desire a different method or outcome?
48
+
49
+ Only act if there's something genuinely worth saving. If nothing stands out, just say 'Nothing to save.' and stop.`;
50
+
51
+ // ─── Flush prompt (ported from flush_memories() in run_agent.py ~L7379) ───
52
+ export const FLUSH_PROMPT = `[System: The session is being compressed. Save anything worth remembering — prioritize user preferences, corrections, and recurring patterns over task-specific details.]`;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Background review — learning loop that auto-saves memory every N turns.
3
+ * Ported from hermes-agent/run_agent.py (_spawn_background_review, _memory_nudge_interval).
4
+ * See PLAN.md → "Hermes Source File Reference Map" for source lines.
5
+ *
6
+ * Uses pi.exec("pi", ["-p", ...]) for isolated one-shot review,
7
+ * keeping us within Pi's intended extension API.
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { MemoryStore } from "../store/memory-store.js";
12
+ import { COMBINED_REVIEW_PROMPT } from "../constants.js";
13
+ import type { MemoryConfig } from "../types.js";
14
+ import { getMessageText } from "../types.js";
15
+
16
+ export function setupBackgroundReview(
17
+ pi: ExtensionAPI,
18
+ store: MemoryStore,
19
+ config: MemoryConfig,
20
+ ): void {
21
+ let turnsSinceReview = 0;
22
+ let userTurnCount = 0;
23
+ let reviewInProgress = false;
24
+
25
+ pi.on("message_end", async (event, _ctx) => {
26
+ if (event.message.role === "user") {
27
+ userTurnCount++;
28
+ }
29
+ });
30
+
31
+ pi.on("turn_end", async (event, ctx) => {
32
+ turnsSinceReview++;
33
+
34
+ if (!config.reviewEnabled) return;
35
+ if (reviewInProgress) return;
36
+ if (turnsSinceReview < config.nudgeInterval) return;
37
+ if (userTurnCount < 3) return;
38
+
39
+ turnsSinceReview = 0;
40
+ reviewInProgress = true;
41
+
42
+ try {
43
+ // Build conversation snapshot from session entries
44
+ const entries = ctx.sessionManager.getBranch();
45
+ const parts: string[] = [];
46
+
47
+ for (const entry of entries) {
48
+ if (entry.type !== "message") continue;
49
+ const msg = entry.message;
50
+ const text = getMessageText(msg);
51
+ if (!text) continue;
52
+ const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
53
+ parts.push(`${prefix}: ${text}`);
54
+ }
55
+ if (parts.length < 4) return; // Not enough conversation to review
56
+
57
+ const currentMemory = store.getMemoryEntries().join("\n§\n");
58
+ const currentUser = store.getUserEntries().join("\n§\n");
59
+
60
+ const reviewPrompt = [
61
+ COMBINED_REVIEW_PROMPT,
62
+ "",
63
+ "--- Current Memory ---",
64
+ currentMemory || "(empty)",
65
+ "",
66
+ "--- Current User Profile ---",
67
+ currentUser || "(empty)",
68
+ "",
69
+ "--- Conversation to Review ---",
70
+ parts.join("\n\n"),
71
+ ].join("\n");
72
+
73
+ const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt], {
74
+ signal: ctx.signal,
75
+ timeout: 60000,
76
+ });
77
+
78
+ if (result.code === 0 && result.stdout) {
79
+ const output = result.stdout.trim();
80
+ if (output && !output.toLowerCase().includes("nothing to save")) {
81
+ ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
82
+ }
83
+ } else {
84
+ ctx.ui.notify(
85
+ `[hermes] auto-review failed (exit=${result.code}): ${result.stderr?.slice(0, 200) || "unknown error"}`,
86
+ "error",
87
+ );
88
+ }
89
+ } catch (err) {
90
+ ctx.ui.notify(`[hermes] auto-review error: ${String(err).slice(0, 200)}`, "error");
91
+ } finally {
92
+ reviewInProgress = false;
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Insights command — /memory-insights shows what's stored in persistent memory.
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { MemoryStore } from "../store/memory-store.js";
7
+
8
+ export function registerInsightsCommand(pi: ExtensionAPI, store: MemoryStore): void {
9
+ pi.registerCommand("memory-insights", {
10
+ description: "Show what's stored in persistent memory",
11
+ handler: async (_args, ctx) => {
12
+ const memoryEntries = store.getMemoryEntries();
13
+ const userEntries = store.getUserEntries();
14
+
15
+ const lines: string[] = [];
16
+ lines.push("");
17
+ lines.push(" ╔══════════════════════════════════════════════╗");
18
+ lines.push(" ║ 🧠 Memory Insights ║");
19
+ lines.push(" ╚══════════════════════════════════════════════╝");
20
+ lines.push("");
21
+
22
+ // Memory section
23
+ lines.push(" 📋 MEMORY (your personal notes)");
24
+ lines.push(" " + "─".repeat(44));
25
+ if (memoryEntries.length === 0) {
26
+ lines.push(" (empty)");
27
+ } else {
28
+ for (let i = 0; i < memoryEntries.length; i++) {
29
+ const preview =
30
+ memoryEntries[i].length > 100
31
+ ? memoryEntries[i].slice(0, 100) + "..."
32
+ : memoryEntries[i];
33
+ lines.push(` ${i + 1}. ${preview}`);
34
+ }
35
+ }
36
+ lines.push("");
37
+
38
+ // User section
39
+ lines.push(" 👤 USER PROFILE");
40
+ lines.push(" " + "─".repeat(44));
41
+ if (userEntries.length === 0) {
42
+ lines.push(" (empty)");
43
+ } else {
44
+ for (let i = 0; i < userEntries.length; i++) {
45
+ const preview =
46
+ userEntries[i].length > 100
47
+ ? userEntries[i].slice(0, 100) + "..."
48
+ : userEntries[i];
49
+ lines.push(` ${i + 1}. ${preview}`);
50
+ }
51
+ }
52
+ lines.push("");
53
+
54
+ ctx.ui.notify(lines.join("\n"), "info");
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Session flush — gives the agent one turn to save memories before context is lost.
3
+ * Ported from hermes-agent/run_agent.py (flush_memories).
4
+ * See PLAN.md → "Hermes Source File Reference Map" for source lines.
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { MemoryStore } from "../store/memory-store.js";
9
+ import { FLUSH_PROMPT } from "../constants.js";
10
+ import type { MemoryConfig } from "../types.js";
11
+ import { getMessageText } from "../types.js";
12
+
13
+ export function setupSessionFlush(
14
+ pi: ExtensionAPI,
15
+ store: MemoryStore,
16
+ config: MemoryConfig,
17
+ ): void {
18
+ let userTurnCount = 0;
19
+
20
+ pi.on("message_end", async (event, _ctx) => {
21
+ if (event.message.role === "user") userTurnCount++;
22
+ });
23
+
24
+ /** Shared flush logic — builds conversation snapshot and spawns pi -p */
25
+ async function flush(ctx: any, signal?: AbortSignal, timeoutMs = 30000): Promise<void> {
26
+ if (userTurnCount < config.flushMinTurns) return;
27
+
28
+ let entries;
29
+ try {
30
+ entries = ctx.sessionManager.getBranch();
31
+ } catch {
32
+ return; // Context already stale
33
+ }
34
+
35
+ const parts: string[] = [];
36
+
37
+ for (const entry of entries) {
38
+ if (entry.type !== "message") continue;
39
+ const msg = entry.message;
40
+ const text = getMessageText(msg);
41
+ if (!text) continue;
42
+ const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
43
+ parts.push(`${prefix}: ${text}`);
44
+ }
45
+ const flushMessage = [
46
+ FLUSH_PROMPT,
47
+ "",
48
+ "--- Conversation ---",
49
+ parts.join("\n\n"),
50
+ ].join("\n");
51
+
52
+ try {
53
+ await pi.exec("pi", ["-p", "--no-session", flushMessage], {
54
+ signal,
55
+ timeout: timeoutMs,
56
+ });
57
+ } catch {
58
+ // Best-effort flush — never block shutdown
59
+ }
60
+ }
61
+
62
+ // Flush before compaction (can afford to wait)
63
+ pi.on("session_before_compact", async (event, ctx) => {
64
+ if (!config.flushOnCompact) return;
65
+ await flush(ctx, event.signal, 30000);
66
+ });
67
+
68
+ // Flush before session shutdown (must be fast, non-blocking)
69
+ pi.on("session_shutdown", async (event, ctx) => {
70
+ if (!config.flushOnShutdown) return;
71
+ // Fire-and-forget with a short timeout so we don't block Pi's shutdown.
72
+ // We intentionally do NOT await — Pi should not wait for the child process.
73
+ flush(ctx, undefined, 10000).catch(() => {});
74
+ });
75
+ }