pi-hermes-memory 0.5.4 → 0.6.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.
- package/docs/0.4/linkedin_post.md +97 -0
- package/docs/0.5/PLAN.md +202 -0
- package/docs/0.5/TASKS.md +144 -0
- package/package.json +1 -1
- package/src/constants.ts +10 -1
- package/src/handlers/background-review.ts +1 -1
- package/src/handlers/correction-detector.ts +29 -0
- package/src/store/memory-store.ts +85 -11
- package/src/store/schema.ts +6 -1
- package/src/store/sqlite-memory-store.ts +111 -14
- package/src/tools/memory-search-tool.ts +11 -5
- package/src/tools/memory-tool.ts +23 -4
- package/src/types.ts +9 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# LinkedIn Post — Pi Hermes Memory v0.4
|
|
2
|
+
|
|
3
|
+
**Best time to post:** 7:30-8:30 AM (morning engagement peak)
|
|
4
|
+
|
|
5
|
+
**Attach:** `docs/images/pi_memory.png` as the post image
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
🚀 **I just open-sourced a persistent memory system for AI coding agents.**
|
|
10
|
+
|
|
11
|
+
Every time you start a new session with an AI coding agent, it forgets everything. That debugging session from last week? Gone. The architecture decision you discussed for 2 hours? Vanished. The user preferences you explained 5 times? You'll explain them a 6th.
|
|
12
|
+
|
|
13
|
+
I got tired of re-explaining context every session. So I built **Pi Hermes Memory** — an extension that gives your AI agent a brain that actually works.
|
|
14
|
+
|
|
15
|
+
**What it does:**
|
|
16
|
+
|
|
17
|
+
🧠 **Persistent memory** — facts, preferences, corrections survive across sessions
|
|
18
|
+
🔍 **Cross-session & cross-project search** — find any conversation across ALL your projects
|
|
19
|
+
📚 **Procedural skills** — the agent saves *how* it solved problems, not just what
|
|
20
|
+
🛡️ **Secret scanning** — API keys and tokens are blocked from being persisted
|
|
21
|
+
⚡ **Background learning** — reviews your conversation every 10 turns and saves what matters
|
|
22
|
+
|
|
23
|
+
**The key insight:**
|
|
24
|
+
|
|
25
|
+
Most memory tools only remember facts. Mine remembers:
|
|
26
|
+
- **What you said** (session history — searchable via FTS5)
|
|
27
|
+
- **What you learned** (episodic memory — builds over time)
|
|
28
|
+
- **How you solved problems** (procedural skills — reusable patterns)
|
|
29
|
+
- **What you corrected** (corrections — saves immediately)
|
|
30
|
+
|
|
31
|
+
And it works **across all your projects**. Ask "what did we discuss about auth?" and it searches every session, every project, instantly.
|
|
32
|
+
|
|
33
|
+
**The architecture:**
|
|
34
|
+
|
|
35
|
+
- Core memory (MEMORY.md): Always in context, 5,000 chars
|
|
36
|
+
- Extended memory (SQLite): Unlimited, searchable on demand
|
|
37
|
+
- Session history (FTS5): Full-text search across all past conversations
|
|
38
|
+
- Skills (SKILL.md): Procedural knowledge that builds over time
|
|
39
|
+
|
|
40
|
+
**The result?**
|
|
41
|
+
|
|
42
|
+
Instead of starting from zero every session, my agent now says:
|
|
43
|
+
|
|
44
|
+
*"Last Tuesday we discussed implementing JWT with refresh tokens. You preferred httpOnly cookies over localStorage. We also decided to use the auth0 SDK. Want me to continue from there?"*
|
|
45
|
+
|
|
46
|
+
**Technical details:**
|
|
47
|
+
- 272 tests
|
|
48
|
+
- SQLite with FTS5 for fast full-text search
|
|
49
|
+
- Hybrid memory: core (always in context) + extended (searchable on demand)
|
|
50
|
+
- Auto-consolidation when memory fills up
|
|
51
|
+
- Correction detection (saves immediately when you correct the agent)
|
|
52
|
+
|
|
53
|
+
It's open source and works with the Pi coding agent.
|
|
54
|
+
|
|
55
|
+
**Get started:**
|
|
56
|
+
```
|
|
57
|
+
pi install npm:pi-hermes-memory
|
|
58
|
+
/memory-index-sessions
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your AI agent should remember as much as you do. Now it does.
|
|
62
|
+
|
|
63
|
+
GitHub: https://github.com/chandra447/pi-hermes-memory
|
|
64
|
+
npm: https://www.npmjs.com/package/pi-hermes-memory
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
#coding-agent #agent-harness #memory #ai
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Posting Tips
|
|
73
|
+
|
|
74
|
+
1. **Attach the logo image** — `docs/images/pi_memory.png`
|
|
75
|
+
2. **Post at 7:30-8:30 AM** — morning coffee + LinkedIn scroll time
|
|
76
|
+
3. **Engage with comments** in the first 2 hours — LinkedIn rewards early engagement
|
|
77
|
+
4. **Reply to everyone** — even simple "thanks!" helps visibility
|
|
78
|
+
5. **Share to relevant groups** if you're in any AI/dev communities
|
|
79
|
+
6. **Tag people** if you know anyone in the Pi community or AI dev space
|
|
80
|
+
|
|
81
|
+
## Hashtag Strategy
|
|
82
|
+
|
|
83
|
+
Primary (high volume):
|
|
84
|
+
- #AI
|
|
85
|
+
- #OpenSource
|
|
86
|
+
- #SoftwareEngineering
|
|
87
|
+
- #DevTools
|
|
88
|
+
|
|
89
|
+
Niche (targeted):
|
|
90
|
+
- #CodingAgent
|
|
91
|
+
- #DeveloperProductivity
|
|
92
|
+
- #MachineLearning
|
|
93
|
+
- #ArtificialIntelligence
|
|
94
|
+
|
|
95
|
+
Brand (discoverable):
|
|
96
|
+
- #Pi
|
|
97
|
+
- #TypeScript
|
package/docs/0.5/PLAN.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# v0.5 Plan: Failure Memory + Categories + Provenance
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Track **failures, corrections, and insights** as first-class memories with full provenance and category labels. Learn from failures like humans do.
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
From X/Twitter feedback:
|
|
10
|
+
> "Memory gets much more useful when it stores failures, not just conversations. I'd want every recalled session to carry provenance: source, timestamp, tool state, and whether the old answer survived contact with reality."
|
|
11
|
+
|
|
12
|
+
## Key Features
|
|
13
|
+
|
|
14
|
+
### 1. Memory Categories
|
|
15
|
+
|
|
16
|
+
Add category labels to differentiate memory types:
|
|
17
|
+
|
|
18
|
+
| Category | What It Is | Example |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `failure` | What didn't work | "Tried localStorage for tokens — XSS risk" |
|
|
21
|
+
| `correction` | User corrected the agent | "Use pnpm, not npm" |
|
|
22
|
+
| `insight` | Learning from experience | "Auth0 SDK handles refresh tokens automatically" |
|
|
23
|
+
| `preference` | User preference | "Prefers dark theme" |
|
|
24
|
+
| `convention` | Project convention | "Monorepo uses turborepo" |
|
|
25
|
+
| `tool-quirk` | Tool-specific knowledge | "CI needs --frozen-lockfile" |
|
|
26
|
+
|
|
27
|
+
### 2. Failure Memory Structure
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
interface FailureMemory {
|
|
31
|
+
category: 'failure' | 'correction' | 'insight' | 'preference' | 'convention' | 'tool-quirk';
|
|
32
|
+
content: string; // What was tried / what happened
|
|
33
|
+
failure_reason?: string; // Why it failed (for failures)
|
|
34
|
+
tool_state?: string; // Relevant tool state (error message, output)
|
|
35
|
+
corrected_to?: string; // What worked instead (if known)
|
|
36
|
+
project: string; // Which project
|
|
37
|
+
session_id?: string; // Which session
|
|
38
|
+
timestamp: string; // When it happened
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Auto-Detect Failures
|
|
43
|
+
|
|
44
|
+
Detect failures from:
|
|
45
|
+
- **Explicit corrections**: "that didn't work", "use X instead", "no, do it this way"
|
|
46
|
+
- **Error messages**: stderr output, test failures, build errors
|
|
47
|
+
- **Agent retries**: When the agent tries multiple approaches
|
|
48
|
+
- **User feedback**: "this is wrong", "that's not right"
|
|
49
|
+
|
|
50
|
+
### 4. Failure Injection into System Prompt
|
|
51
|
+
|
|
52
|
+
Inject relevant recent failures into the system prompt at session start:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
<memory-context>
|
|
56
|
+
RECENT FAILURES & LESSONS (learn from these):
|
|
57
|
+
• [failure] Tried: localStorage for JWT tokens — Failed: XSS vulnerability
|
|
58
|
+
→ Corrected to: httpOnly cookies with SameSite=Strict
|
|
59
|
+
• [correction] Use pnpm, not npm (corrected 2 days ago)
|
|
60
|
+
• [insight] Auth0 SDK handles refresh tokens automatically — no manual implementation needed
|
|
61
|
+
═══ END MEMORY ═══
|
|
62
|
+
</memory-context>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Injection rules:**
|
|
66
|
+
- Only inject failures from last 7 days
|
|
67
|
+
- Only inject failures relevant to current project (or global)
|
|
68
|
+
- Max 5 failure entries to avoid prompt bloat
|
|
69
|
+
- Separate `<memory-context>` block from regular memory
|
|
70
|
+
|
|
71
|
+
### 5. Search with Categories
|
|
72
|
+
|
|
73
|
+
Update `memory_search` tool to support category filtering:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
memory_search("auth", category: "failure") → Past auth failures
|
|
77
|
+
memory_search("deploy", category: "convention") → Deploy conventions
|
|
78
|
+
memory_search("typescript", category: "tool-quirk") → TS tool quirks
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 6. Store Failures in SQLite
|
|
82
|
+
|
|
83
|
+
Failures stored in `memories` table with `target: 'failure'`:
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
-- New target type
|
|
87
|
+
target TEXT CHECK (target IN ('memory', 'user', 'failure'))
|
|
88
|
+
|
|
89
|
+
-- Content stored as JSON with category
|
|
90
|
+
{
|
|
91
|
+
"category": "failure",
|
|
92
|
+
"content": "Tried localStorage for JWT tokens",
|
|
93
|
+
"failure_reason": "XSS vulnerability - tokens accessible via JS",
|
|
94
|
+
"tool_state": "Error: Token exposed in browser console",
|
|
95
|
+
"corrected_to": "httpOnly cookies with SameSite=Strict",
|
|
96
|
+
"project": "my-app",
|
|
97
|
+
"timestamp": "2026-05-03T10:30:00Z"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 7. Update Background Review Prompt
|
|
102
|
+
|
|
103
|
+
Enhance the background review prompt to extract failures:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Review this conversation and extract:
|
|
107
|
+
|
|
108
|
+
1. FAILURES: What was tried but didn't work?
|
|
109
|
+
- What was attempted
|
|
110
|
+
- Why it failed
|
|
111
|
+
- What error occurred
|
|
112
|
+
- What worked instead (if found)
|
|
113
|
+
|
|
114
|
+
2. CORRECTIONS: Did the user correct the agent?
|
|
115
|
+
- What was wrong
|
|
116
|
+
- What is correct
|
|
117
|
+
|
|
118
|
+
3. INSIGHTS: What was learned?
|
|
119
|
+
- New knowledge about tools, APIs, patterns
|
|
120
|
+
- Project-specific learnings
|
|
121
|
+
|
|
122
|
+
4. CONVENTIONS: Any project conventions discovered?
|
|
123
|
+
- Coding style, naming, patterns
|
|
124
|
+
- Tool preferences
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Architecture Changes
|
|
128
|
+
|
|
129
|
+
### Memory Store (`src/store/memory-store.ts`)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Add to MemoryStore class
|
|
133
|
+
addFailure(content: string, options: {
|
|
134
|
+
category: MemoryCategory;
|
|
135
|
+
failureReason?: string;
|
|
136
|
+
toolState?: string;
|
|
137
|
+
correctedTo?: string;
|
|
138
|
+
}): void;
|
|
139
|
+
|
|
140
|
+
getFailureEntries(): string[]; // Returns recent failures for injection
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### SQLite Memory Store (`src/store/sqlite-memory-store.ts`)
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Update searchMemories to support category filter
|
|
147
|
+
searchMemories(db, query, { project, target, category, limit })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Memory Search Tool (`src/tools/memory-search-tool.ts`)
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Add category parameter
|
|
154
|
+
category: Type.Optional(StringEnum([
|
|
155
|
+
'failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk'
|
|
156
|
+
] as const, { description: 'Filter by memory category.' }))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Background Review (`src/handlers/background-review.ts`)
|
|
160
|
+
|
|
161
|
+
- Extract failures during review
|
|
162
|
+
- Store with category labels
|
|
163
|
+
- Include failure context in review prompt
|
|
164
|
+
|
|
165
|
+
### Correction Detector (`src/handlers/correction-detector.ts`)
|
|
166
|
+
|
|
167
|
+
- Extract failure context when correction detected
|
|
168
|
+
- Store what was wrong + what is correct
|
|
169
|
+
|
|
170
|
+
### System Prompt Injection (`src/index.ts`)
|
|
171
|
+
|
|
172
|
+
- Add separate `<memory-context>` block for failures
|
|
173
|
+
- Inject only recent (7 days) and relevant (project match)
|
|
174
|
+
- Max 5 entries
|
|
175
|
+
|
|
176
|
+
## Files to Change
|
|
177
|
+
|
|
178
|
+
| File | Change |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `src/types.ts` | Add `MemoryCategory` type |
|
|
181
|
+
| `src/constants.ts` | Update review prompt for failure extraction |
|
|
182
|
+
| `src/store/memory-store.ts` | Add `addFailure()`, `getFailureEntries()` |
|
|
183
|
+
| `src/store/sqlite-memory-store.ts` | Add category support to search |
|
|
184
|
+
| `src/tools/memory-search-tool.ts` | Add category parameter |
|
|
185
|
+
| `src/handlers/background-review.ts` | Extract failures during review |
|
|
186
|
+
| `src/handlers/correction-detector.ts` | Store failure context on corrections |
|
|
187
|
+
| `src/index.ts` | Inject failure memories into system prompt |
|
|
188
|
+
| `tests/store/memory-store.test.ts` | Test failure storage |
|
|
189
|
+
| `tests/store/sqlite-memory-store.test.ts` | Test category search |
|
|
190
|
+
| `tests/tools/memory-search-tool.test.ts` | Test category parameter |
|
|
191
|
+
|
|
192
|
+
## Complexity Assessment
|
|
193
|
+
|
|
194
|
+
- **Effort**: Medium (2-3 hours)
|
|
195
|
+
- **Risk**: Low (additive, no breaking changes)
|
|
196
|
+
- **Tests**: ~15 new tests
|
|
197
|
+
|
|
198
|
+
## Migration
|
|
199
|
+
|
|
200
|
+
- Existing memories get `category: null` (no migration needed)
|
|
201
|
+
- New memories get category assigned
|
|
202
|
+
- Search works with or without category filter
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# v0.5 Tasks: Failure Memory + Categories + Provenance
|
|
2
|
+
|
|
3
|
+
## Status Legend
|
|
4
|
+
- `[ ]` Not started
|
|
5
|
+
- `[~]` In progress
|
|
6
|
+
- `[x]` Done
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Epic 1: Category Types & Schema
|
|
11
|
+
|
|
12
|
+
### Task 1.1: Add MemoryCategory type
|
|
13
|
+
- [ ] Add `MemoryCategory` type to `src/types.ts`
|
|
14
|
+
- [ ] Categories: `failure`, `correction`, `insight`, `preference`, `convention`, `tool-quirk`
|
|
15
|
+
|
|
16
|
+
### Task 1.2: Update SQLite schema
|
|
17
|
+
- [ ] Update `src/store/schema.ts` — add `category` column to memories table
|
|
18
|
+
- [ ] Add index on category for faster filtering
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Epic 2: Failure Memory Store
|
|
23
|
+
|
|
24
|
+
### Task 2.1: Update MemoryStore
|
|
25
|
+
- [ ] Add `addFailure()` method to `src/store/memory-store.ts`
|
|
26
|
+
- [ ] Store failures in MEMORY.md with category metadata
|
|
27
|
+
- [ ] Add `getFailureEntries()` to retrieve recent failures (last 7 days)
|
|
28
|
+
- [ ] Update `formatForSystemPrompt()` to include failure section
|
|
29
|
+
|
|
30
|
+
### Task 2.2: Update SQLite Memory Store
|
|
31
|
+
- [ ] Add category support to `src/store/sqlite-memory-store.ts`
|
|
32
|
+
- [ ] Update `addMemory()` to accept category
|
|
33
|
+
- [ ] Update `searchMemories()` to filter by category
|
|
34
|
+
- [ ] Add `getRecentFailures()` method
|
|
35
|
+
|
|
36
|
+
### Task 2.3: Tests
|
|
37
|
+
- [ ] Test `addFailure()` in `tests/store/memory-store.test.ts`
|
|
38
|
+
- [ ] Test category filtering in `tests/store/sqlite-memory-store.test.ts`
|
|
39
|
+
- [ ] Test `getRecentFailures()` returns only last 7 days
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Epic 3: Failure Detection
|
|
44
|
+
|
|
45
|
+
### Task 3.1: Update Correction Detector
|
|
46
|
+
- [ ] Update `src/handlers/correction-detector.ts`
|
|
47
|
+
- [ ] Extract failure context when correction detected
|
|
48
|
+
- [ ] Store: what was wrong, what is correct, category
|
|
49
|
+
|
|
50
|
+
### Task 3.2: Update Background Review Prompt
|
|
51
|
+
- [ ] Update `src/constants.ts` — enhance `CONSOLIDATION_PROMPT`
|
|
52
|
+
- [ ] Add failure extraction instructions
|
|
53
|
+
- [ ] Prompt to categorize findings (failure, correction, insight, etc.)
|
|
54
|
+
|
|
55
|
+
### Task 3.3: Auto-Detect Failures in Conversation
|
|
56
|
+
- [ ] Add failure pattern detection in `src/handlers/background-review.ts`
|
|
57
|
+
- [ ] Detect: "that didn't work", "use X instead", error messages
|
|
58
|
+
- [ ] Store failures with `tool_state` (error output)
|
|
59
|
+
|
|
60
|
+
### Task 3.4: Tests
|
|
61
|
+
- [ ] Test failure detection in `tests/handlers/correction-detector.test.ts`
|
|
62
|
+
- [ ] Test failure extraction in `tests/handlers/background-review.test.ts`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Epic 4: Failure Injection
|
|
67
|
+
|
|
68
|
+
### Task 4.1: Update System Prompt Injection
|
|
69
|
+
- [ ] Update `src/index.ts` — add failure memory block
|
|
70
|
+
- [ ] Separate `<memory-context>` block for failures
|
|
71
|
+
- [ ] Only inject recent (7 days) and relevant (project match)
|
|
72
|
+
- [ ] Max 5 failure entries
|
|
73
|
+
|
|
74
|
+
### Task 4.2: Failure Injection Format
|
|
75
|
+
- [ ] Format:
|
|
76
|
+
```
|
|
77
|
+
RECENT FAILURES & LESSONS (learn from these):
|
|
78
|
+
• [failure] Tried: X — Failed: Y → Corrected to: Z
|
|
79
|
+
• [correction] Use X, not Y (corrected N days ago)
|
|
80
|
+
• [insight] Learned: X
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Task 4.3: Tests
|
|
84
|
+
- [ ] Test failure injection in `tests/handlers/system-prompt.test.ts`
|
|
85
|
+
- [ ] Test: only recent failures injected
|
|
86
|
+
- [ ] Test: max 5 entries enforced
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Epic 5: Search with Categories
|
|
91
|
+
|
|
92
|
+
### Task 5.1: Update Memory Search Tool
|
|
93
|
+
- [ ] Update `src/tools/memory-search-tool.ts`
|
|
94
|
+
- [ ] Add `category` parameter using `StringEnum`
|
|
95
|
+
- [ ] Categories: `failure`, `correction`, `insight`, `preference`, `convention`, `tool-quirk`
|
|
96
|
+
|
|
97
|
+
### Task 5.2: Update Search Implementation
|
|
98
|
+
- [ ] Update `src/store/sqlite-memory-store.ts` — `searchMemories()`
|
|
99
|
+
- [ ] Filter by category when provided
|
|
100
|
+
- [ ] Include category in search results
|
|
101
|
+
|
|
102
|
+
### Task 5.3: Tests
|
|
103
|
+
- [ ] Test category search in `tests/store/sqlite-memory-store.test.ts`
|
|
104
|
+
- [ ] Test: `memory_search("auth", category: "failure")` returns failures only
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Epic 6: Integration & Polish
|
|
109
|
+
|
|
110
|
+
### Task 6.1: Wire Everything Together
|
|
111
|
+
- [ ] Update `src/index.ts` — ensure all components work together
|
|
112
|
+
- [ ] Test end-to-end flow: detect → store → inject → search
|
|
113
|
+
|
|
114
|
+
### Task 6.2: Update README
|
|
115
|
+
- [ ] Add "Learning from Failures" section
|
|
116
|
+
- [ ] Document categories and how they work
|
|
117
|
+
- [ ] Add examples of failure memory
|
|
118
|
+
|
|
119
|
+
### Task 6.3: Version Bump
|
|
120
|
+
- [ ] Bump to `0.5.5`
|
|
121
|
+
- [ ] Run full test suite
|
|
122
|
+
- [ ] Publish to npm
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Summary
|
|
127
|
+
|
|
128
|
+
| Epic | Files Changed | Tests |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 1. Category Types | types.ts, schema.ts | 2 |
|
|
131
|
+
| 2. Failure Store | memory-store.ts, sqlite-memory-store.ts | 6 |
|
|
132
|
+
| 3. Failure Detection | correction-detector.ts, background-review.ts, constants.ts | 4 |
|
|
133
|
+
| 4. Failure Injection | index.ts | 3 |
|
|
134
|
+
| 5. Category Search | memory-search-tool.ts, sqlite-memory-store.ts | 3 |
|
|
135
|
+
| 6. Integration | index.ts, README.md | — |
|
|
136
|
+
| **Total** | **8 files** | **~18 tests** |
|
|
137
|
+
|
|
138
|
+
## Implementation Order
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Epic 1 (Types) → Epic 2 (Store) → Epic 3 (Detection) → Epic 4 (Injection) → Epic 5 (Search) → Epic 6 (Polish)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each epic builds on the previous one. Epics 3-5 can be done in parallel after Epic 2.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
package/src/constants.ts
CHANGED
|
@@ -45,10 +45,19 @@ THREE TARGETS:
|
|
|
45
45
|
ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).`;
|
|
46
46
|
|
|
47
47
|
// ─── Background review prompt (ported from _COMBINED_REVIEW_PROMPT in run_agent.py ~L2855) ───
|
|
48
|
-
export const COMBINED_REVIEW_PROMPT = `Review the conversation above and consider
|
|
48
|
+
export const COMBINED_REVIEW_PROMPT = `Review the conversation above and consider these aspects:
|
|
49
49
|
|
|
50
50
|
**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.
|
|
51
51
|
|
|
52
|
+
**Failures & Corrections**: Did anything fail or go wrong? Extract these as failure memories:
|
|
53
|
+
- [failure] What was tried but didn't work? (e.g., "Used localStorage for tokens — XSS vulnerability")
|
|
54
|
+
- [correction] Did the user correct you? (e.g., "Use pnpm, not npm")
|
|
55
|
+
- [insight] What was learned from the experience?
|
|
56
|
+
- [convention] Any project conventions discovered?
|
|
57
|
+
- [tool-quirk] Any tool-specific knowledge gained?
|
|
58
|
+
|
|
59
|
+
For failures, include: what was tried, why it failed, what error occurred, and what worked instead.
|
|
60
|
+
|
|
52
61
|
**Skills**: Was a complex, non-trivial approach used to complete a task — one that required trial and error, multiple tool calls, or changing course? If so, save a reusable procedure using the skill tool with action 'create'. Include: when to use it, step-by-step procedure, pitfalls to avoid, and how to verify success. If a related skill already exists, use action 'patch' to update it instead of creating a duplicate.
|
|
53
62
|
|
|
54
63
|
Only act if there's something genuinely worth saving. If nothing stands out, just say 'Nothing to save.' and stop.`;
|
|
@@ -111,7 +111,7 @@ export function setupBackgroundReview(
|
|
|
111
111
|
|
|
112
112
|
const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt.join("\n")], {
|
|
113
113
|
signal: ctx.signal,
|
|
114
|
-
timeout:
|
|
114
|
+
timeout: 120000,
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
if (result.code === 0 && result.stdout) {
|
|
@@ -20,6 +20,19 @@ import {
|
|
|
20
20
|
import type { MemoryConfig } from "../types.js";
|
|
21
21
|
import { getMessageText } from "../types.js";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Extract the directive part from a correction message.
|
|
25
|
+
* E.g., "no, use pnpm instead" -> "use pnpm instead"
|
|
26
|
+
*/
|
|
27
|
+
function extractCorrectionDirective(text: string): string {
|
|
28
|
+
// Remove common correction starters
|
|
29
|
+
const cleaned = text
|
|
30
|
+
.replace(/^(no|wrong|actually|stop|don'?t|that'?s not|I said|I told you)[,\.\s!]+/i, '')
|
|
31
|
+
.replace(/^(please\s+)?/i, '')
|
|
32
|
+
.trim();
|
|
33
|
+
return cleaned || text;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* Check if a user message is a correction using the two-pass filter.
|
|
25
38
|
* Returns true if the message should trigger an immediate save.
|
|
@@ -147,6 +160,22 @@ export function setupCorrectionDetector(
|
|
|
147
160
|
ctx.ui.notify("🔧 Correction detected — memory updated", "info");
|
|
148
161
|
}
|
|
149
162
|
}
|
|
163
|
+
|
|
164
|
+
// Also save as a failure memory for learning
|
|
165
|
+
try {
|
|
166
|
+
const lastUserMsg = recentParts.find(p => p.startsWith("[USER]"));
|
|
167
|
+
const correctionText = lastUserMsg ? lastUserMsg.replace(/^\[USER\]:\s*/, "") : "";
|
|
168
|
+
if (correctionText) {
|
|
169
|
+
const directive = extractCorrectionDirective(correctionText);
|
|
170
|
+
await store.addFailure(directive, {
|
|
171
|
+
category: "correction",
|
|
172
|
+
failureReason: "User corrected the agent",
|
|
173
|
+
project: projectStore ? "project" : undefined,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Best-effort — don't block the session
|
|
178
|
+
}
|
|
150
179
|
} catch {
|
|
151
180
|
// Best-effort — don't block the session
|
|
152
181
|
} finally {
|
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
MEMORY_FILE,
|
|
23
23
|
USER_FILE,
|
|
24
24
|
} from "../constants.js";
|
|
25
|
-
import type { MemoryConfig, MemoryResult, MemorySnapshot, ConsolidationResult } from "../types.js";
|
|
25
|
+
import type { MemoryConfig, MemoryResult, MemorySnapshot, ConsolidationResult, MemoryCategory } from "../types.js";
|
|
26
26
|
|
|
27
27
|
export class MemoryStore {
|
|
28
28
|
private memoryEntries: string[] = [];
|
|
29
29
|
private userEntries: string[] = [];
|
|
30
|
+
private failureEntries: string[] = [];
|
|
30
31
|
private snapshot: MemorySnapshot = { memory: "", user: "" };
|
|
31
32
|
private consolidator: ((target: "memory" | "user", signal?: AbortSignal) => Promise<ConsolidationResult>) | null = null;
|
|
32
33
|
|
|
@@ -46,24 +47,30 @@ export class MemoryStore {
|
|
|
46
47
|
return this.config.memoryDir ?? path.join(os.homedir(), ".pi", "agent", "memory");
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
private pathFor(target: "memory" | "user"): string {
|
|
50
|
-
return path.join(this.memoryDir,
|
|
50
|
+
private pathFor(target: "memory" | "user" | "failure"): string {
|
|
51
|
+
if (target === "user") return path.join(this.memoryDir, USER_FILE);
|
|
52
|
+
if (target === "failure") return path.join(this.memoryDir, "failures.md");
|
|
53
|
+
return path.join(this.memoryDir, MEMORY_FILE);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
private entriesFor(target: "memory" | "user"): string[] {
|
|
54
|
-
|
|
56
|
+
private entriesFor(target: "memory" | "user" | "failure"): string[] {
|
|
57
|
+
if (target === "user") return this.userEntries;
|
|
58
|
+
if (target === "failure") return this.failureEntries;
|
|
59
|
+
return this.memoryEntries;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
private setEntries(target: "memory" | "user", entries: string[]): void {
|
|
62
|
+
private setEntries(target: "memory" | "user" | "failure", entries: string[]): void {
|
|
58
63
|
if (target === "user") this.userEntries = entries;
|
|
64
|
+
else if (target === "failure") this.failureEntries = entries;
|
|
59
65
|
else this.memoryEntries = entries;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
private charLimit(target: "memory" | "user"): number {
|
|
68
|
+
private charLimit(target: "memory" | "user" | "failure"): number {
|
|
69
|
+
if (target === "failure") return this.config.memoryCharLimit * 2; // Failures get more space
|
|
63
70
|
return target === "user" ? this.config.userCharLimit : this.config.memoryCharLimit;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
private charCount(target: "memory" | "user"): number {
|
|
73
|
+
private charCount(target: "memory" | "user" | "failure"): number {
|
|
67
74
|
const entries = this.entriesFor(target);
|
|
68
75
|
return entries.length ? entries.join(ENTRY_DELIMITER).length : 0;
|
|
69
76
|
}
|
|
@@ -74,10 +81,12 @@ export class MemoryStore {
|
|
|
74
81
|
await fs.mkdir(this.memoryDir, { recursive: true });
|
|
75
82
|
this.memoryEntries = await this.readFile(this.pathFor("memory"));
|
|
76
83
|
this.userEntries = await this.readFile(this.pathFor("user"));
|
|
84
|
+
this.failureEntries = await this.readFile(this.pathFor("failure"));
|
|
77
85
|
|
|
78
86
|
// Deduplicate preserving order
|
|
79
87
|
this.memoryEntries = [...new Set(this.memoryEntries)];
|
|
80
88
|
this.userEntries = [...new Set(this.userEntries)];
|
|
89
|
+
this.failureEntries = [...new Set(this.failureEntries)];
|
|
81
90
|
|
|
82
91
|
// Capture frozen snapshot for system prompt injection
|
|
83
92
|
// Strip metadata comments — the LLM doesn't need to see timestamps
|
|
@@ -95,7 +104,55 @@ export class MemoryStore {
|
|
|
95
104
|
return this._add(target, content, signal);
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
|
|
107
|
+
async addFailure(content: string, options: {
|
|
108
|
+
category: MemoryCategory;
|
|
109
|
+
failureReason?: string;
|
|
110
|
+
toolState?: string;
|
|
111
|
+
correctedTo?: string;
|
|
112
|
+
project?: string;
|
|
113
|
+
}): Promise<MemoryResult> {
|
|
114
|
+
content = content.trim();
|
|
115
|
+
if (!content) return { success: false, error: "Content cannot be empty." };
|
|
116
|
+
|
|
117
|
+
const scanError = scanContent(content);
|
|
118
|
+
if (scanError) return { success: false, error: scanError };
|
|
119
|
+
|
|
120
|
+
const categoryTag = "[" + options.category + "]";
|
|
121
|
+
const parts = [categoryTag + " " + content];
|
|
122
|
+
if (options.failureReason) parts.push("Failed: " + options.failureReason);
|
|
123
|
+
if (options.toolState) parts.push("Tool state: " + options.toolState);
|
|
124
|
+
if (options.correctedTo) parts.push("Corrected to: " + options.correctedTo);
|
|
125
|
+
if (options.project) parts.push("Project: " + options.project);
|
|
126
|
+
|
|
127
|
+
const failureText = parts.join(" — ");
|
|
128
|
+
const today = new Date().toISOString().split("T")[0];
|
|
129
|
+
const encoded = this.encodeEntry(failureText, today, today);
|
|
130
|
+
|
|
131
|
+
this.failureEntries.push(encoded);
|
|
132
|
+
await this.saveToDisk("failure");
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
target: "failure",
|
|
137
|
+
message: "Failure memory saved: " + options.category,
|
|
138
|
+
entry_count: this.failureEntries.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getFailureEntries(maxAgeDays = 7): string[] {
|
|
143
|
+
const cutoff = new Date();
|
|
144
|
+
cutoff.setDate(cutoff.getDate() - maxAgeDays);
|
|
145
|
+
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
146
|
+
|
|
147
|
+
return this.failureEntries
|
|
148
|
+
.filter((entry) => {
|
|
149
|
+
const decoded = this.decodeEntry(entry);
|
|
150
|
+
return decoded.created >= cutoffStr;
|
|
151
|
+
})
|
|
152
|
+
.map((entry) => this.stripMetadata(entry));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async _add(target: "memory" | "user" | "failure", content: string, signal?: AbortSignal, _retriesLeft = 1): Promise<MemoryResult> {
|
|
99
156
|
content = content.trim();
|
|
100
157
|
if (!content) return { success: false, error: "Content cannot be empty." };
|
|
101
158
|
|
|
@@ -221,6 +278,16 @@ export class MemoryStore {
|
|
|
221
278
|
const parts: string[] = [];
|
|
222
279
|
if (this.snapshot.memory) parts.push(this.fenceBlock(this.snapshot.memory));
|
|
223
280
|
if (this.snapshot.user) parts.push(this.fenceBlock(this.snapshot.user));
|
|
281
|
+
|
|
282
|
+
// Add recent failure memories
|
|
283
|
+
const recentFailures = this.getFailureEntries(7);
|
|
284
|
+
if (recentFailures.length > 0) {
|
|
285
|
+
const maxFailures = 5;
|
|
286
|
+
const failures = recentFailures.slice(0, maxFailures);
|
|
287
|
+
const failureBlock = this.renderFailureBlock(failures);
|
|
288
|
+
parts.push(this.fenceBlock(failureBlock));
|
|
289
|
+
}
|
|
290
|
+
|
|
224
291
|
return parts.join("\n\n");
|
|
225
292
|
}
|
|
226
293
|
|
|
@@ -270,7 +337,7 @@ export class MemoryStore {
|
|
|
270
337
|
return this.decodeEntry(text).text;
|
|
271
338
|
}
|
|
272
339
|
|
|
273
|
-
private successResponse(target: "memory" | "user", message?: string): MemoryResult {
|
|
340
|
+
private successResponse(target: "memory" | "user" | "failure", message?: string): MemoryResult {
|
|
274
341
|
const entries = this.entriesFor(target);
|
|
275
342
|
const current = this.charCount(target);
|
|
276
343
|
const limit = this.charLimit(target);
|
|
@@ -333,6 +400,13 @@ export class MemoryStore {
|
|
|
333
400
|
return `${separator}\n${header}\n${separator}\n${content}`;
|
|
334
401
|
}
|
|
335
402
|
|
|
403
|
+
private renderFailureBlock(entries: string[]): string {
|
|
404
|
+
if (!entries.length) return "";
|
|
405
|
+
const header = "RECENT FAILURES & LESSONS (learn from these):";
|
|
406
|
+
const bulletList = entries.map((e) => "• " + e).join("\n");
|
|
407
|
+
return `${header}\n${bulletList}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
336
410
|
private async readFile(filePath: string): Promise<string[]> {
|
|
337
411
|
try {
|
|
338
412
|
const raw = await fs.readFile(filePath, "utf-8");
|
|
@@ -344,7 +418,7 @@ export class MemoryStore {
|
|
|
344
418
|
}
|
|
345
419
|
|
|
346
420
|
/** Atomic write: temp file + fs.rename() — same crash-safety as Hermes. */
|
|
347
|
-
private async saveToDisk(target: "memory" | "user"): Promise<void> {
|
|
421
|
+
private async saveToDisk(target: "memory" | "user" | "failure"): Promise<void> {
|
|
348
422
|
const filePath = this.pathFor(target);
|
|
349
423
|
const entries = this.entriesFor(target);
|
|
350
424
|
const content = entries.length ? entries.join(ENTRY_DELIMITER) : "";
|
package/src/store/schema.ts
CHANGED
|
@@ -56,8 +56,12 @@ export const SCHEMA_SQL = `
|
|
|
56
56
|
CREATE TABLE IF NOT EXISTS memories (
|
|
57
57
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
58
|
project TEXT,
|
|
59
|
-
target TEXT NOT NULL CHECK (target IN ('memory', 'user')),
|
|
59
|
+
target TEXT NOT NULL CHECK (target IN ('memory', 'user', 'failure')),
|
|
60
|
+
category TEXT CHECK (category IN ('failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk')),
|
|
60
61
|
content TEXT NOT NULL,
|
|
62
|
+
failure_reason TEXT,
|
|
63
|
+
tool_state TEXT,
|
|
64
|
+
corrected_to TEXT,
|
|
61
65
|
created DATE NOT NULL,
|
|
62
66
|
last_referenced DATE NOT NULL
|
|
63
67
|
);
|
|
@@ -89,6 +93,7 @@ export const SCHEMA_SQL = `
|
|
|
89
93
|
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
90
94
|
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
91
95
|
CREATE INDEX IF NOT EXISTS idx_memories_target ON memories(target);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
92
97
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
|
|
93
98
|
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at);
|
|
94
99
|
`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
+
import type { MemoryCategory } from '../types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A memory entry stored in SQLite.
|
|
@@ -6,8 +7,12 @@ import { DatabaseManager } from './db.js';
|
|
|
6
7
|
export interface SqliteMemoryEntry {
|
|
7
8
|
id: number;
|
|
8
9
|
project: string | null;
|
|
9
|
-
target: 'memory' | 'user';
|
|
10
|
+
target: 'memory' | 'user' | 'failure';
|
|
11
|
+
category: MemoryCategory | null;
|
|
10
12
|
content: string;
|
|
13
|
+
failureReason: string | null;
|
|
14
|
+
toolState: string | null;
|
|
15
|
+
correctedTo: string | null;
|
|
11
16
|
created: string;
|
|
12
17
|
lastReferenced: string;
|
|
13
18
|
}
|
|
@@ -18,22 +23,30 @@ export interface SqliteMemoryEntry {
|
|
|
18
23
|
export function addMemory(
|
|
19
24
|
dbManager: DatabaseManager,
|
|
20
25
|
content: string,
|
|
21
|
-
target: 'memory' | 'user' = 'memory',
|
|
22
|
-
project: string | null = null
|
|
26
|
+
target: 'memory' | 'user' | 'failure' = 'memory',
|
|
27
|
+
project: string | null = null,
|
|
28
|
+
category: MemoryCategory | null = null,
|
|
29
|
+
failureReason: string | null = null,
|
|
30
|
+
toolState: string | null = null,
|
|
31
|
+
correctedTo: string | null = null
|
|
23
32
|
): SqliteMemoryEntry {
|
|
24
33
|
const db = dbManager.getDb();
|
|
25
34
|
const today = new Date().toISOString().split('T')[0];
|
|
26
35
|
|
|
27
36
|
const result = db.prepare(`
|
|
28
|
-
INSERT INTO memories (project, target, content, created, last_referenced)
|
|
29
|
-
VALUES (?, ?, ?, ?, ?)
|
|
30
|
-
`).run(project, target, content, today, today);
|
|
37
|
+
INSERT INTO memories (project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
39
|
+
`).run(project, target, category, content, failureReason, toolState, correctedTo, today, today);
|
|
31
40
|
|
|
32
41
|
return {
|
|
33
42
|
id: Number(result.lastInsertRowid),
|
|
34
43
|
project,
|
|
35
44
|
target,
|
|
45
|
+
category,
|
|
36
46
|
content,
|
|
47
|
+
failureReason,
|
|
48
|
+
toolState,
|
|
49
|
+
correctedTo,
|
|
37
50
|
created: today,
|
|
38
51
|
lastReferenced: today,
|
|
39
52
|
};
|
|
@@ -58,10 +71,10 @@ function escapeFts5Query(query: string): string {
|
|
|
58
71
|
export function searchMemories(
|
|
59
72
|
dbManager: DatabaseManager,
|
|
60
73
|
query: string,
|
|
61
|
-
options: { project?: string; target?: string; limit?: number } = {}
|
|
74
|
+
options: { project?: string; target?: string; category?: MemoryCategory; limit?: number } = {}
|
|
62
75
|
): SqliteMemoryEntry[] {
|
|
63
76
|
const db = dbManager.getDb();
|
|
64
|
-
const { project, target, limit = 10 } = options;
|
|
77
|
+
const { project, target, category, limit = 10 } = options;
|
|
65
78
|
|
|
66
79
|
const conditions: string[] = [];
|
|
67
80
|
const params: unknown[] = [];
|
|
@@ -84,10 +97,15 @@ export function searchMemories(
|
|
|
84
97
|
params.push(target);
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
if (category) {
|
|
101
|
+
conditions.push('m.category = ?');
|
|
102
|
+
params.push(category);
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
88
106
|
|
|
89
107
|
const sql = `
|
|
90
|
-
SELECT id, project, target, content, created, last_referenced
|
|
108
|
+
SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
|
|
91
109
|
FROM memories m
|
|
92
110
|
${whereClause}
|
|
93
111
|
ORDER BY m.last_referenced DESC
|
|
@@ -99,7 +117,11 @@ export function searchMemories(
|
|
|
99
117
|
id: number;
|
|
100
118
|
project: string | null;
|
|
101
119
|
target: string;
|
|
120
|
+
category: string | null;
|
|
102
121
|
content: string;
|
|
122
|
+
failure_reason: string | null;
|
|
123
|
+
tool_state: string | null;
|
|
124
|
+
corrected_to: string | null;
|
|
103
125
|
created: string;
|
|
104
126
|
last_referenced: string;
|
|
105
127
|
}>;
|
|
@@ -107,8 +129,12 @@ export function searchMemories(
|
|
|
107
129
|
return rows.map(row => ({
|
|
108
130
|
id: row.id,
|
|
109
131
|
project: row.project,
|
|
110
|
-
target: row.target as 'memory' | 'user',
|
|
132
|
+
target: row.target as 'memory' | 'user' | 'failure',
|
|
133
|
+
category: row.category as MemoryCategory | null,
|
|
111
134
|
content: row.content,
|
|
135
|
+
failureReason: row.failure_reason,
|
|
136
|
+
toolState: row.tool_state,
|
|
137
|
+
correctedTo: row.corrected_to,
|
|
112
138
|
created: row.created,
|
|
113
139
|
lastReferenced: row.last_referenced,
|
|
114
140
|
}));
|
|
@@ -119,10 +145,10 @@ export function searchMemories(
|
|
|
119
145
|
*/
|
|
120
146
|
export function getMemories(
|
|
121
147
|
dbManager: DatabaseManager,
|
|
122
|
-
options: { project?: string | null; target?: string } = {}
|
|
148
|
+
options: { project?: string | null; target?: string; category?: MemoryCategory } = {}
|
|
123
149
|
): SqliteMemoryEntry[] {
|
|
124
150
|
const db = dbManager.getDb();
|
|
125
|
-
const { project, target } = options;
|
|
151
|
+
const { project, target, category } = options;
|
|
126
152
|
|
|
127
153
|
const conditions: string[] = [];
|
|
128
154
|
const params: unknown[] = [];
|
|
@@ -141,10 +167,15 @@ export function getMemories(
|
|
|
141
167
|
params.push(target);
|
|
142
168
|
}
|
|
143
169
|
|
|
170
|
+
if (category) {
|
|
171
|
+
conditions.push('category = ?');
|
|
172
|
+
params.push(category);
|
|
173
|
+
}
|
|
174
|
+
|
|
144
175
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
145
176
|
|
|
146
177
|
const rows = db.prepare(`
|
|
147
|
-
SELECT id, project, target, content, created, last_referenced
|
|
178
|
+
SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
|
|
148
179
|
FROM memories
|
|
149
180
|
${whereClause}
|
|
150
181
|
ORDER BY last_referenced DESC
|
|
@@ -152,7 +183,11 @@ export function getMemories(
|
|
|
152
183
|
id: number;
|
|
153
184
|
project: string | null;
|
|
154
185
|
target: string;
|
|
186
|
+
category: string | null;
|
|
155
187
|
content: string;
|
|
188
|
+
failure_reason: string | null;
|
|
189
|
+
tool_state: string | null;
|
|
190
|
+
corrected_to: string | null;
|
|
156
191
|
created: string;
|
|
157
192
|
last_referenced: string;
|
|
158
193
|
}>;
|
|
@@ -160,8 +195,12 @@ export function getMemories(
|
|
|
160
195
|
return rows.map(row => ({
|
|
161
196
|
id: row.id,
|
|
162
197
|
project: row.project,
|
|
163
|
-
target: row.target as 'memory' | 'user',
|
|
198
|
+
target: row.target as 'memory' | 'user' | 'failure',
|
|
199
|
+
category: row.category as MemoryCategory | null,
|
|
164
200
|
content: row.content,
|
|
201
|
+
failureReason: row.failure_reason,
|
|
202
|
+
toolState: row.tool_state,
|
|
203
|
+
correctedTo: row.corrected_to,
|
|
165
204
|
created: row.created,
|
|
166
205
|
lastReferenced: row.last_referenced,
|
|
167
206
|
}));
|
|
@@ -176,6 +215,64 @@ export function removeMemory(dbManager: DatabaseManager, id: number): boolean {
|
|
|
176
215
|
return result.changes > 0;
|
|
177
216
|
}
|
|
178
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Get recent failure memories (last N days).
|
|
220
|
+
*/
|
|
221
|
+
export function getRecentFailures(
|
|
222
|
+
dbManager: DatabaseManager,
|
|
223
|
+
maxAgeDays = 7,
|
|
224
|
+
project?: string | null
|
|
225
|
+
): SqliteMemoryEntry[] {
|
|
226
|
+
const db = dbManager.getDb();
|
|
227
|
+
const cutoff = new Date();
|
|
228
|
+
cutoff.setDate(cutoff.getDate() - maxAgeDays);
|
|
229
|
+
const cutoffStr = cutoff.toISOString().split('T')[0];
|
|
230
|
+
|
|
231
|
+
const conditions: string[] = ['target = ?', 'created >= ?'];
|
|
232
|
+
const params: unknown[] = ['failure', cutoffStr];
|
|
233
|
+
|
|
234
|
+
if (project !== undefined) {
|
|
235
|
+
if (project === null) {
|
|
236
|
+
conditions.push('project IS NULL');
|
|
237
|
+
} else {
|
|
238
|
+
conditions.push('(project = ? OR project IS NULL)');
|
|
239
|
+
params.push(project);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rows = db.prepare(`
|
|
244
|
+
SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
|
|
245
|
+
FROM memories
|
|
246
|
+
WHERE ${conditions.join(' AND ')}
|
|
247
|
+
ORDER BY created DESC
|
|
248
|
+
LIMIT 5
|
|
249
|
+
`).all(...params) as Array<{
|
|
250
|
+
id: number;
|
|
251
|
+
project: string | null;
|
|
252
|
+
target: string;
|
|
253
|
+
category: string | null;
|
|
254
|
+
content: string;
|
|
255
|
+
failure_reason: string | null;
|
|
256
|
+
tool_state: string | null;
|
|
257
|
+
corrected_to: string | null;
|
|
258
|
+
created: string;
|
|
259
|
+
last_referenced: string;
|
|
260
|
+
}>;
|
|
261
|
+
|
|
262
|
+
return rows.map(row => ({
|
|
263
|
+
id: row.id,
|
|
264
|
+
project: row.project,
|
|
265
|
+
target: row.target as 'memory' | 'user' | 'failure',
|
|
266
|
+
category: row.category as MemoryCategory | null,
|
|
267
|
+
content: row.content,
|
|
268
|
+
failureReason: row.failure_reason,
|
|
269
|
+
toolState: row.tool_state,
|
|
270
|
+
correctedTo: row.corrected_to,
|
|
271
|
+
created: row.created,
|
|
272
|
+
lastReferenced: row.last_referenced,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
179
276
|
/**
|
|
180
277
|
* Update a memory's last_referenced date.
|
|
181
278
|
*/
|
|
@@ -3,6 +3,7 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
4
4
|
import { DatabaseManager } from '../store/db.js';
|
|
5
5
|
import { searchMemories, getMemoryStats } from '../store/sqlite-memory-store.js';
|
|
6
|
+
import type { MemoryCategory } from '../types.js';
|
|
6
7
|
|
|
7
8
|
interface SearchResult {
|
|
8
9
|
success: boolean;
|
|
@@ -21,23 +22,27 @@ Use cases:
|
|
|
21
22
|
- Find memories about a specific topic: "What do I know about auth setup?"
|
|
22
23
|
- Search project-specific memories: "What conventions does project X follow?"
|
|
23
24
|
- Find user preferences: "What are the user's testing preferences?"
|
|
25
|
+
- Search for past failures: "memory_search('auth', category='failure')"
|
|
24
26
|
|
|
25
27
|
Returns matching memory entries with project context and dates.`,
|
|
26
28
|
promptSnippet: 'Search extended memory store (unlimited capacity)',
|
|
27
29
|
promptGuidelines: [
|
|
28
30
|
'Use memory_search when you need context beyond what is in the system prompt.',
|
|
29
31
|
'Use memory_search to find project-specific memories or user preferences.',
|
|
32
|
+
'Use memory_search with category filter to find specific types of memories (failure, correction, insight, etc.).',
|
|
30
33
|
],
|
|
31
34
|
parameters: Type.Object({
|
|
32
35
|
query: Type.String({ description: 'Search query. Use natural language or specific terms.' }),
|
|
33
36
|
project: Type.Optional(Type.String({ description: 'Filter by project name. Pass null for global memories only.' })),
|
|
34
|
-
target: Type.Optional(StringEnum(['memory', 'user'] as const, { description: 'Filter by target type (memory or
|
|
37
|
+
target: Type.Optional(StringEnum(['memory', 'user', 'failure'] as const, { description: 'Filter by target type (memory, user, or failure).' })),
|
|
38
|
+
category: Type.Optional(StringEnum(['failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk'] as const, { description: 'Filter by memory category.' })),
|
|
35
39
|
limit: Type.Optional(Type.Number({ description: 'Maximum results to return (default: 10, max: 20).' })),
|
|
36
40
|
}),
|
|
37
|
-
execute: async (_id: string, args: { query: string; project?: string; target?: string; limit?: number }) => {
|
|
41
|
+
execute: async (_id: string, args: { query: string; project?: string; target?: string; category?: string; limit?: number }) => {
|
|
38
42
|
const query = args.query;
|
|
39
43
|
const project = args.project;
|
|
40
44
|
const target = args.target;
|
|
45
|
+
const category = args.category as MemoryCategory | undefined;
|
|
41
46
|
const limit = Math.min(args.limit || 10, 20);
|
|
42
47
|
|
|
43
48
|
if (!query || query.trim().length === 0) {
|
|
@@ -51,7 +56,7 @@ Returns matching memory entries with project context and dates.`,
|
|
|
51
56
|
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
const results = searchMemories(dbManager, query, { project, target, limit });
|
|
59
|
+
const results = searchMemories(dbManager, query, { project, target, category, limit });
|
|
55
60
|
|
|
56
61
|
if (results.length === 0) {
|
|
57
62
|
const result: SearchResult = { success: true, count: 0, message: `No memories found matching "${query}". Try a different search term or broader query.` };
|
|
@@ -62,8 +67,9 @@ Returns matching memory entries with project context and dates.`,
|
|
|
62
67
|
|
|
63
68
|
for (const entry of results) {
|
|
64
69
|
const projectLabel = entry.project ? `[${entry.project}]` : '[global]';
|
|
65
|
-
const targetLabel = entry.target === 'user' ? '👤' : '🧠';
|
|
66
|
-
|
|
70
|
+
const targetLabel = entry.target === 'user' ? '👤' : entry.target === 'failure' ? '⚠️' : '🧠';
|
|
71
|
+
const categoryLabel = entry.category ? ` [${entry.category}]` : '';
|
|
72
|
+
output += `${targetLabel} ${projectLabel}${categoryLabel} ${entry.content}\n`;
|
|
67
73
|
output += ` Created: ${entry.created} | Last used: ${entry.lastReferenced}\n\n`;
|
|
68
74
|
}
|
|
69
75
|
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Type } from "typebox";
|
|
|
9
9
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
10
10
|
import { MemoryStore } from "../store/memory-store.js";
|
|
11
11
|
import { MEMORY_TOOL_DESCRIPTION } from "../constants.js";
|
|
12
|
+
import type { MemoryCategory } from "../types.js";
|
|
12
13
|
|
|
13
14
|
export function registerMemoryTool(pi: ExtensionAPI, store: MemoryStore, projectStore: MemoryStore | null): void {
|
|
14
15
|
pi.registerTool({
|
|
@@ -21,10 +22,11 @@ export function registerMemoryTool(pi: ExtensionAPI, store: MemoryStore, project
|
|
|
21
22
|
"Use the memory tool proactively when the user corrects you, shares a preference, or reveals personal details worth remembering.",
|
|
22
23
|
"Use the memory tool when you discover environment facts, project conventions, or reusable patterns useful in future sessions.",
|
|
23
24
|
"Do NOT use memory for temporary task state, TODO items, or session progress — only for durable, cross-session facts.",
|
|
25
|
+
"Use target='failure' with category to save what didn't work (failures, corrections, insights).",
|
|
24
26
|
],
|
|
25
27
|
parameters: Type.Object({
|
|
26
28
|
action: StringEnum(["add", "replace", "remove"] as const),
|
|
27
|
-
target: StringEnum(["memory", "user", "project"] as const),
|
|
29
|
+
target: StringEnum(["memory", "user", "project", "failure"] as const),
|
|
28
30
|
content: Type.Optional(
|
|
29
31
|
Type.String({ description: "Entry content for add/replace" })
|
|
30
32
|
),
|
|
@@ -34,12 +36,20 @@ export function registerMemoryTool(pi: ExtensionAPI, store: MemoryStore, project
|
|
|
34
36
|
"Substring identifying entry for replace/remove",
|
|
35
37
|
})
|
|
36
38
|
),
|
|
39
|
+
category: Type.Optional(
|
|
40
|
+
StringEnum(["failure", "correction", "insight", "preference", "convention", "tool-quirk"] as const, {
|
|
41
|
+
description: "Category for failure memories",
|
|
42
|
+
})
|
|
43
|
+
),
|
|
44
|
+
failure_reason: Type.Optional(
|
|
45
|
+
Type.String({ description: "Why it failed (for failure category)" })
|
|
46
|
+
),
|
|
37
47
|
}),
|
|
38
48
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
39
|
-
const { action, target: rawTarget, content, old_text } = params;
|
|
49
|
+
const { action, target: rawTarget, content, old_text, category, failure_reason } = params;
|
|
40
50
|
|
|
41
51
|
// Route 'project' to projectStore (internal target 'memory')
|
|
42
|
-
const target = rawTarget as "memory" | "user";
|
|
52
|
+
const target = rawTarget as "memory" | "user" | "failure";
|
|
43
53
|
const activeStore = rawTarget === "project" ? projectStore : store;
|
|
44
54
|
|
|
45
55
|
if (rawTarget === "project" && !projectStore) {
|
|
@@ -69,7 +79,16 @@ export function registerMemoryTool(pi: ExtensionAPI, store: MemoryStore, project
|
|
|
69
79
|
details: {},
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
|
-
|
|
82
|
+
// Handle failure target with category
|
|
83
|
+
if (rawTarget === "failure") {
|
|
84
|
+
const memoryCategory = (category || "failure") as MemoryCategory;
|
|
85
|
+
result = await store_.addFailure(content, {
|
|
86
|
+
category: memoryCategory,
|
|
87
|
+
failureReason: failure_reason,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
result = await store_.add(target, content);
|
|
91
|
+
}
|
|
73
92
|
break;
|
|
74
93
|
|
|
75
94
|
case "replace":
|
package/src/types.ts
CHANGED
|
@@ -35,11 +35,19 @@ export interface MemoryConfig {
|
|
|
35
35
|
sessionRetentionDays?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type MemoryCategory =
|
|
39
|
+
| "failure"
|
|
40
|
+
| "correction"
|
|
41
|
+
| "insight"
|
|
42
|
+
| "preference"
|
|
43
|
+
| "convention"
|
|
44
|
+
| "tool-quirk";
|
|
45
|
+
|
|
38
46
|
export interface MemoryResult {
|
|
39
47
|
success: boolean;
|
|
40
48
|
error?: string;
|
|
41
49
|
message?: string;
|
|
42
|
-
target?: "memory" | "user";
|
|
50
|
+
target?: "memory" | "user" | "failure";
|
|
43
51
|
entries?: string[];
|
|
44
52
|
usage?: string;
|
|
45
53
|
entry_count?: number;
|