pi-hermes-memory 0.5.4 β†’ 0.6.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.
package/README.md CHANGED
@@ -38,6 +38,7 @@ pi install npm:pi-hermes-memory
38
38
  |---|---|
39
39
  | πŸ” **Session Search** | Search across all past conversations via SQLite FTS5 |
40
40
  | 🧠 **Persistent Memory** | Facts, preferences, lessons saved to markdown files |
41
+ | ⚠️ **Failure Memory** | Learn from failures β€” stores what didn't work and why |
41
42
  | πŸ“š **Procedural Skills** | The agent saves *how* it solved problems as reusable docs |
42
43
  | ⚑ **Background Learning** | Every 10 turns (or 15 tool calls) the agent reviews and saves |
43
44
  | πŸ”§ **Correction Detection** | When you correct the agent, it saves immediately |
@@ -124,9 +125,51 @@ System Prompt
124
125
  β”‚ β€’ tests use node:test with tsx β”‚
125
126
  β”‚ ═══ END MEMORY ═══ β”‚
126
127
  β”‚ </memory-context> β”‚
128
+ β”‚ β”‚
129
+ β”‚ <memory-context> β”‚
130
+ β”‚ RECENT FAILURES & LESSONS (learn from): β”‚
131
+ β”‚ β€’ [correction] Use pnpm, not npm β”‚
132
+ β”‚ β€’ [failure] Tried localStorage β€” XSS β”‚
133
+ β”‚ β€’ [insight] Auth0 handles refresh tokensβ”‚
134
+ β”‚ ═══ END MEMORY ═══ β”‚
135
+ β”‚ </memory-context> β”‚
127
136
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
128
137
  ```
129
138
 
139
+ ## Failure Memory
140
+
141
+ The agent learns from failures, corrections, and insights β€” just like humans do.
142
+
143
+ ### Memory Categories
144
+
145
+ | Category | What it stores | Example |
146
+ |---|---|---|
147
+ | `failure` | What didn't work and why | "Tried localStorage for tokens β€” XSS vulnerability" |
148
+ | `correction` | User corrections | "Use pnpm, not npm" |
149
+ | `insight` | Learnings from experience | "Auth0 SDK handles refresh tokens automatically" |
150
+ | `preference` | User preferences | "Prefers dark theme" |
151
+ | `convention` | Project conventions | "Monorepo uses turborepo" |
152
+ | `tool-quirk` | Tool-specific knowledge | "CI needs --frozen-lockfile" |
153
+
154
+ ### How It Works
155
+
156
+ 1. **Auto-detection**: Background review extracts failures from conversations
157
+ 2. **Correction capture**: When you correct the agent, it saves what went wrong
158
+ 3. **System prompt injection**: Recent failures (last 7 days) are injected at session start
159
+ 4. **Searchable**: Use `memory_search("auth", category: "failure")` to find past failures
160
+
161
+ ### Example
162
+
163
+ ```
164
+ User: No, use pnpm not npm
165
+ Agent: [saves correction memory]
166
+
167
+ Next session:
168
+ Agent: "I remember you prefer pnpm over npm. Let me use that."
169
+ ```
170
+
171
+ The agent learns from its mistakes so you don't have to repeat yourself.
172
+
130
173
  Memory blocks are wrapped in `<memory-context>` XML tags with a guard note ("NOT new user input") to prevent the LLM from treating stored facts as instructions.
131
174
 
132
175
  ## Usage
@@ -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
@@ -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.5.4",
3
+ "version": "0.6.1",
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 two things:
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.`;
@@ -15,7 +15,7 @@ import type { ConsolidationResult } from "../types.js";
15
15
  export async function triggerConsolidation(
16
16
  pi: ExtensionAPI,
17
17
  store: MemoryStore,
18
- target: "memory" | "user",
18
+ target: "memory" | "user" | "failure",
19
19
  signal?: AbortSignal,
20
20
  ): Promise<ConsolidationResult> {
21
21
  const entries =
@@ -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: 60000,
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,13 +22,14 @@ 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
- private consolidator: ((target: "memory" | "user", signal?: AbortSignal) => Promise<ConsolidationResult>) | null = null;
32
+ private consolidator: ((target: "memory" | "user" | "failure", signal?: AbortSignal) => Promise<ConsolidationResult>) | null = null;
32
33
 
33
34
  constructor(private config: MemoryConfig) {}
34
35
 
@@ -36,7 +37,7 @@ export class MemoryStore {
36
37
  * Inject a consolidation function (avoids circular imports).
37
38
  * Called from index.ts after both store and pi are available.
38
39
  */
39
- setConsolidator(fn: (target: "memory" | "user", signal?: AbortSignal) => Promise<ConsolidationResult>): void {
40
+ setConsolidator(fn: (target: "memory" | "user" | "failure", signal?: AbortSignal) => Promise<ConsolidationResult>): void {
40
41
  this.consolidator = fn;
41
42
  }
42
43
 
@@ -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, target === "user" ? USER_FILE : MEMORY_FILE);
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
- return target === "user" ? this.userEntries : this.memoryEntries;
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
@@ -91,11 +100,59 @@ export class MemoryStore {
91
100
 
92
101
  // ─── CRUD ───
93
102
 
94
- async add(target: "memory" | "user", content: string, signal?: AbortSignal): Promise<MemoryResult> {
103
+ async add(target: "memory" | "user" | "failure", content: string, signal?: AbortSignal): Promise<MemoryResult> {
95
104
  return this._add(target, content, signal);
96
105
  }
97
106
 
98
- private async _add(target: "memory" | "user", content: string, signal?: AbortSignal, _retriesLeft = 1): Promise<MemoryResult> {
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
 
@@ -145,7 +202,7 @@ export class MemoryStore {
145
202
  return this.successResponse(target, "Entry added.");
146
203
  }
147
204
 
148
- async replace(target: "memory" | "user", oldText: string, newContent: string): Promise<MemoryResult> {
205
+ async replace(target: "memory" | "user" | "failure", oldText: string, newContent: string): Promise<MemoryResult> {
149
206
  oldText = oldText.trim();
150
207
  newContent = newContent.trim();
151
208
  if (!oldText) return { success: false, error: "old_text cannot be empty." };
@@ -191,7 +248,7 @@ export class MemoryStore {
191
248
  return this.successResponse(target, "Entry replaced.");
192
249
  }
193
250
 
194
- async remove(target: "memory" | "user", oldText: string): Promise<MemoryResult> {
251
+ async remove(target: "memory" | "user" | "failure", oldText: string): Promise<MemoryResult> {
195
252
  oldText = oldText.trim();
196
253
  if (!oldText) return { success: false, error: "old_text cannot be empty." };
197
254
 
@@ -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) : "";
@@ -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 user).' })),
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
- output += `${targetLabel} ${projectLabel} ${entry.content}\n`;
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
 
@@ -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
- result = await store_.add(target, content);
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;