pi-hermes-memory 0.6.9 → 0.7.2

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.
@@ -13,7 +13,7 @@ import {
13
13
  } from '../store/sqlite-memory-store.js';
14
14
  import { ENTRY_DELIMITER, MEMORY_FILE, USER_FILE } from '../constants.js';
15
15
 
16
- interface BackfillCounters {
16
+ export interface BackfillCounters {
17
17
  filesScanned: number;
18
18
  entriesScanned: number;
19
19
  imported: number;
@@ -50,60 +50,92 @@ function importEntries(
50
50
  }
51
51
  }
52
52
 
53
- function scanProjectDirs(agentRoot: string, globalDir: string): Array<{ name: string; memoryFile: string }> {
54
- if (!fs.existsSync(agentRoot)) return [];
53
+ function scanProjectDirs(agentRoot: string, globalDir: string, projectsMemoryDir = "projects-memory"): Array<{ name: string; memoryFile: string }> {
54
+ const projectsRoot = path.join(agentRoot, projectsMemoryDir);
55
+ const projects = new Map<string, string>();
56
+
57
+ if (fs.existsSync(projectsRoot)) {
58
+ for (const name of fs.readdirSync(projectsRoot)) {
59
+ const dir = path.join(projectsRoot, name);
60
+ const memoryFile = path.join(dir, MEMORY_FILE);
61
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
62
+ projects.set(name, memoryFile);
63
+ }
64
+ }
65
+ }
66
+
55
67
  const globalDirName = path.basename(globalDir);
68
+ if (fs.existsSync(agentRoot)) {
69
+ for (const name of fs.readdirSync(agentRoot)) {
70
+ if (name === globalDirName || name === projectsMemoryDir || name === 'skills' || name.startsWith('.')) continue;
71
+ if (projects.has(name)) continue;
72
+ const dir = path.join(agentRoot, name);
73
+ const memoryFile = path.join(dir, MEMORY_FILE);
74
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
75
+ projects.set(name, memoryFile);
76
+ }
77
+ }
78
+ }
56
79
 
57
- return fs.readdirSync(agentRoot)
58
- .map((name) => ({ name, dir: path.join(agentRoot, name) }))
59
- .filter(({ name, dir }) => name !== globalDirName && fs.existsSync(dir) && fs.statSync(dir).isDirectory())
60
- .map(({ name, dir }) => ({ name, memoryFile: path.join(dir, MEMORY_FILE) }))
80
+ return [...projects.entries()]
81
+ .map(([name, memoryFile]) => ({ name, memoryFile }))
61
82
  .filter(({ memoryFile }) => fs.existsSync(memoryFile));
62
83
  }
63
84
 
85
+ export function syncMarkdownMemoriesToSqlite(
86
+ dbManager: DatabaseManager,
87
+ globalDir: string,
88
+ projectsMemoryDir?: string,
89
+ ): BackfillCounters & { projectCount: number } {
90
+ const counters: BackfillCounters = {
91
+ filesScanned: 0,
92
+ entriesScanned: 0,
93
+ imported: 0,
94
+ skipped: 0,
95
+ warnings: [],
96
+ };
97
+
98
+ const globalMemoryFile = path.join(globalDir, MEMORY_FILE);
99
+ const globalUserFile = path.join(globalDir, USER_FILE);
100
+ const globalFailureFile = path.join(globalDir, 'failures.md');
101
+
102
+ const importFile = (
103
+ filePath: string,
104
+ target: 'memory' | 'user' | 'failure',
105
+ project: string | null = null,
106
+ ) => {
107
+ if (!fs.existsSync(filePath)) return;
108
+ counters.filesScanned++;
109
+ const entries = readEntries(filePath);
110
+ importEntries(dbManager, counters, entries, target, project);
111
+ };
112
+
113
+ importFile(globalMemoryFile, 'memory');
114
+ importFile(globalUserFile, 'user');
115
+ importFile(globalFailureFile, 'failure');
116
+
117
+ const agentRoot = path.dirname(globalDir);
118
+ const projects = scanProjectDirs(agentRoot, globalDir, projectsMemoryDir);
119
+ for (const project of projects) {
120
+ importFile(project.memoryFile, 'memory', project.name);
121
+ }
122
+
123
+ return { ...counters, projectCount: projects.length };
124
+ }
125
+
64
126
  export function registerSyncMarkdownMemoriesCommand(
65
127
  pi: ExtensionAPI,
66
128
  dbManager: DatabaseManager,
67
129
  globalDir: string,
130
+ projectsMemoryDir?: string,
68
131
  ): void {
69
132
  pi.registerCommand('memory-sync-markdown', {
70
133
  description: 'Backfill Markdown memories into the SQLite search store',
71
134
  handler: async (_args, ctx: ExtensionCommandContext) => {
72
- const counters: BackfillCounters = {
73
- filesScanned: 0,
74
- entriesScanned: 0,
75
- imported: 0,
76
- skipped: 0,
77
- warnings: [],
78
- };
79
-
80
135
  ctx.ui.notify('🔄 Scanning Markdown memory files for SQLite backfill...', 'info');
81
136
 
82
137
  try {
83
- const globalMemoryFile = path.join(globalDir, MEMORY_FILE);
84
- const globalUserFile = path.join(globalDir, USER_FILE);
85
- const globalFailureFile = path.join(globalDir, 'failures.md');
86
-
87
- const importFile = (
88
- filePath: string,
89
- target: 'memory' | 'user' | 'failure',
90
- project: string | null = null,
91
- ) => {
92
- if (!fs.existsSync(filePath)) return;
93
- counters.filesScanned++;
94
- const entries = readEntries(filePath);
95
- importEntries(dbManager, counters, entries, target, project);
96
- };
97
-
98
- importFile(globalMemoryFile, 'memory');
99
- importFile(globalUserFile, 'user');
100
- importFile(globalFailureFile, 'failure');
101
-
102
- const agentRoot = path.dirname(globalDir);
103
- const projects = scanProjectDirs(agentRoot, globalDir);
104
- for (const project of projects) {
105
- importFile(project.memoryFile, 'memory', project.name);
106
- }
138
+ const counters = syncMarkdownMemoriesToSqlite(dbManager, globalDir, projectsMemoryDir);
107
139
 
108
140
  let output = `\n✅ Markdown → SQLite sync complete!\n\n`;
109
141
  output += `📊 Results:\n`;
@@ -112,8 +144,8 @@ export function registerSyncMarkdownMemoriesCommand(
112
144
  output += `├─ Imported into SQLite: ${counters.imported}\n`;
113
145
  output += `└─ Skipped as duplicates: ${counters.skipped}\n`;
114
146
 
115
- if (projects.length > 0) {
116
- output += `\n📁 Project memories scanned: ${projects.length}\n`;
147
+ if (counters.projectCount > 0) {
148
+ output += `\n📁 Project memories scanned: ${counters.projectCount}\n`;
117
149
  }
118
150
 
119
151
  if (counters.warnings.length > 0) {
package/src/index.ts CHANGED
@@ -45,10 +45,12 @@ import { registerInterviewCommand } from "./handlers/interview.js";
45
45
  import { registerSwitchProjectCommand } from "./handlers/switch-project.js";
46
46
  import { registerIndexSessionsCommand } from "./handlers/index-sessions.js";
47
47
  import { registerLearnMemoryCommand } from "./handlers/learn-memory.js";
48
- import { registerSyncMarkdownMemoriesCommand } from "./handlers/sync-markdown-memories.js";
48
+ import { registerSyncMarkdownMemoriesCommand, syncMarkdownMemoriesToSqlite } from "./handlers/sync-markdown-memories.js";
49
49
  import { registerPreviewContextCommand } from "./handlers/preview-context.js";
50
50
  import { loadConfig } from "./config.js";
51
51
  import { detectProject } from "./project.js";
52
+ import { buildPromptContext } from "./prompt-context.js";
53
+ import { migrateLegacyProjectMemoryDirs } from "./project-memory-migration.js";
52
54
 
53
55
  export default function (pi: ExtensionAPI) {
54
56
  const config = loadConfig();
@@ -58,10 +60,20 @@ export default function (pi: ExtensionAPI) {
58
60
  const skillStore = new SkillStore(path.join(globalDir, "skills"));
59
61
  const dbManager = new DatabaseManager(globalDir);
60
62
 
63
+ // Keep project memory available for users upgrading from the old
64
+ // ~/.pi/agent/<project>/ layout. This is non-destructive: legacy folders
65
+ // remain in place while entries are copied/merged into projects-memory/.
66
+ migrateLegacyProjectMemoryDirs(globalDir, config.projectsMemoryDir);
67
+ try {
68
+ syncMarkdownMemoriesToSqlite(dbManager, globalDir, config.projectsMemoryDir);
69
+ } catch {
70
+ // Best-effort only: failed SQLite backfill should not block extension startup.
71
+ }
72
+
61
73
  // Detect project from cwd using shared helper
62
- const project = detectProject();
74
+ const project = detectProject(config.projectsMemoryDir);
63
75
 
64
- // Project-scoped store: ~/.pi/agent/<project_name>/
76
+ // Project-scoped store: ~/.pi/agent/<projectsMemoryDir>/<project_name>/
65
77
  const projectConfig = project.memoryDir
66
78
  ? { ...config, memoryCharLimit: config.projectCharLimit, memoryDir: project.memoryDir }
67
79
  : { ...config, memoryDir: undefined };
@@ -74,20 +86,13 @@ export default function (pi: ExtensionAPI) {
74
86
  if (projectStore) await projectStore.loadFromDisk();
75
87
  });
76
88
 
77
- // ── 2. Inject frozen snapshot + skill index + project memory into system prompt ──
89
+ // ── 2. Inject memory policy by default; legacy mode keeps full frozen memory blocks ──
78
90
  pi.on("before_agent_start", async (event, _ctx) => {
79
- const memoryBlock = store.formatForSystemPrompt();
80
- const skillIndex = await skillStore.formatIndexForSystemPrompt();
81
- const projectBlock = projectStore ? projectStore.formatProjectBlock(projectName) : "";
82
-
83
- const parts: string[] = [];
84
- if (memoryBlock) parts.push(memoryBlock);
85
- if (projectBlock) parts.push(projectBlock);
86
- if (skillIndex) parts.push(skillIndex);
91
+ const promptContext = await buildPromptContext(config, store, projectStore, skillStore, projectName);
87
92
 
88
- if (parts.length > 0) {
93
+ if (promptContext) {
89
94
  return {
90
- systemPrompt: event.systemPrompt + "\n\n" + parts.join("\n\n"),
95
+ systemPrompt: event.systemPrompt + "\n\n" + promptContext,
91
96
  };
92
97
  }
93
98
  });
@@ -111,7 +116,7 @@ export default function (pi: ExtensionAPI) {
111
116
  registerConsolidateCommand(pi, store);
112
117
 
113
118
  // ── 8. Setup correction detection ──
114
- setupCorrectionDetector(pi, store, projectStore, config, dbManager);
119
+ setupCorrectionDetector(pi, store, projectStore, config, dbManager, projectName);
115
120
 
116
121
  // ── 9. Setup skill auto-trigger ──
117
122
  setupSkillAutoTrigger(pi, store, skillStore, config);
@@ -120,10 +125,10 @@ export default function (pi: ExtensionAPI) {
120
125
  registerInsightsCommand(pi, store, projectStore, projectName);
121
126
  registerSkillsCommand(pi, skillStore);
122
127
  registerInterviewCommand(pi, store);
123
- registerSwitchProjectCommand(pi);
128
+ registerSwitchProjectCommand(pi, config);
124
129
  registerLearnMemoryCommand(pi);
125
- registerSyncMarkdownMemoriesCommand(pi, dbManager, globalDir);
126
- registerPreviewContextCommand(pi, store, projectStore, skillStore, projectName);
130
+ registerSyncMarkdownMemoriesCommand(pi, dbManager, globalDir, config.projectsMemoryDir);
131
+ registerPreviewContextCommand(pi, store, projectStore, skillStore, projectName, config.memoryMode);
127
132
 
128
133
  // ── 11. SQLite session search + extended memory ──
129
134
  registerSessionSearchTool(pi, dbManager);
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ENTRY_DELIMITER, MEMORY_FILE } from "./constants.js";
4
+
5
+ export interface ProjectMemoryMigrationResult {
6
+ scanned: number;
7
+ copied: number;
8
+ merged: number;
9
+ skipped: number;
10
+ warnings: string[];
11
+ }
12
+
13
+ function readEntries(filePath: string): string[] {
14
+ if (!fs.existsSync(filePath)) return [];
15
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
16
+ if (!raw) return [];
17
+ return raw.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
18
+ }
19
+
20
+ function writeEntries(filePath: string, entries: string[]): void {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, entries.join(ENTRY_DELIMITER), "utf-8");
23
+ }
24
+
25
+ function isLegacyProjectDir(agentRoot: string, projectsMemoryDir: string, name: string): boolean {
26
+ if (name === "memory" || name === "skills" || name === projectsMemoryDir) return false;
27
+ if (name.startsWith(".")) return false;
28
+
29
+ const dir = path.join(agentRoot, name);
30
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(path.join(dir, MEMORY_FILE));
31
+ }
32
+
33
+ export function migrateLegacyProjectMemoryDirs(
34
+ globalDir: string,
35
+ projectsMemoryDir = "projects-memory",
36
+ ): ProjectMemoryMigrationResult {
37
+ const result: ProjectMemoryMigrationResult = {
38
+ scanned: 0,
39
+ copied: 0,
40
+ merged: 0,
41
+ skipped: 0,
42
+ warnings: [],
43
+ };
44
+
45
+ const agentRoot = path.dirname(globalDir);
46
+ if (!fs.existsSync(agentRoot)) return result;
47
+
48
+ const projectsRoot = path.join(agentRoot, projectsMemoryDir);
49
+
50
+ for (const name of fs.readdirSync(agentRoot)) {
51
+ if (!isLegacyProjectDir(agentRoot, projectsMemoryDir, name)) continue;
52
+ result.scanned++;
53
+
54
+ const legacyFile = path.join(agentRoot, name, MEMORY_FILE);
55
+ const targetFile = path.join(projectsRoot, name, MEMORY_FILE);
56
+
57
+ try {
58
+ const legacyEntries = readEntries(legacyFile);
59
+ if (legacyEntries.length === 0) {
60
+ result.skipped++;
61
+ continue;
62
+ }
63
+
64
+ if (!fs.existsSync(targetFile)) {
65
+ writeEntries(targetFile, legacyEntries);
66
+ result.copied++;
67
+ continue;
68
+ }
69
+
70
+ const targetEntries = readEntries(targetFile);
71
+ const mergedEntries = [...targetEntries];
72
+ const seen = new Set(targetEntries);
73
+
74
+ for (const entry of legacyEntries) {
75
+ if (!seen.has(entry)) {
76
+ seen.add(entry);
77
+ mergedEntries.push(entry);
78
+ }
79
+ }
80
+
81
+ if (mergedEntries.length === targetEntries.length) {
82
+ result.skipped++;
83
+ continue;
84
+ }
85
+
86
+ writeEntries(targetFile, mergedEntries);
87
+ result.merged++;
88
+ } catch (err) {
89
+ result.warnings.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
91
+ }
92
+
93
+ return result;
94
+ }
package/src/project.ts CHANGED
@@ -18,9 +18,9 @@ export interface ProjectInfo {
18
18
  *
19
19
  * A "project" is any directory that is not the user's home directory.
20
20
  * The project name is the directory's basename.
21
- * Project-scoped memory is stored at ~/.pi/agent/<projectName>/.
21
+ * Project-scoped memory is stored at ~/.pi/agent/<projectsMemoryDir>/<projectName>/.
22
22
  */
23
- export function detectProject(cwd?: string): ProjectInfo {
23
+ export function detectProject(projectsMemoryDir = "projects-memory", cwd?: string): ProjectInfo {
24
24
  const dir = cwd ?? process.cwd();
25
25
  const homeDir = os.homedir();
26
26
 
@@ -39,6 +39,6 @@ export function detectProject(cwd?: string): ProjectInfo {
39
39
 
40
40
  return {
41
41
  name,
42
- memoryDir: path.join(homeDir, ".pi", "agent", name),
42
+ memoryDir: path.join(homeDir, ".pi", "agent", projectsMemoryDir, name),
43
43
  };
44
44
  }
@@ -0,0 +1,27 @@
1
+ import { MEMORY_POLICY_PROMPT } from "./constants.js";
2
+ import type { MemoryConfig } from "./types.js";
3
+ import type { MemoryStore } from "./store/memory-store.js";
4
+ import type { SkillStore } from "./store/skill-store.js";
5
+
6
+ export async function buildPromptContext(
7
+ config: Pick<MemoryConfig, "memoryMode">,
8
+ store: MemoryStore,
9
+ projectStore: MemoryStore | null,
10
+ skillStore: SkillStore,
11
+ projectName: string,
12
+ ): Promise<string> {
13
+ if (config.memoryMode === "policy-only") {
14
+ return MEMORY_POLICY_PROMPT;
15
+ }
16
+
17
+ const memoryBlock = store.formatForSystemPrompt();
18
+ const skillIndex = await skillStore.formatIndexForSystemPrompt();
19
+ const projectBlock = projectStore ? projectStore.formatProjectBlock(projectName) : "";
20
+
21
+ const parts: string[] = [];
22
+ if (memoryBlock) parts.push(memoryBlock);
23
+ if (projectBlock) parts.push(projectBlock);
24
+ if (skillIndex) parts.push(skillIndex);
25
+
26
+ return parts.join("\n\n");
27
+ }
@@ -71,7 +71,7 @@ export function scanContent(content: string): string | null {
71
71
  // Check threat patterns
72
72
  for (const { pattern, id } of MEMORY_THREAT_PATTERNS) {
73
73
  if (pattern.test(content)) {
74
- return `Blocked: content matches threat pattern '${id}'. Memory entries are injected into the system prompt and must not contain injection or exfiltration payloads.`;
74
+ return `Blocked: content matches threat pattern '${id}'. Memory entries may be surfaced through search or legacy prompt injection and must not contain injection or exfiltration payloads.`;
75
75
  }
76
76
  }
77
77
 
@@ -424,13 +424,19 @@ export class MemoryStore {
424
424
  }
425
425
  }
426
426
 
427
- /** Atomic write: temp file + fs.rename() — same crash-safety as Hermes. */
427
+ /**
428
+ * Atomic write: temp file + fs.rename().
429
+ * Creates temp files in the same directory as the target to avoid
430
+ * cross-device rename errors (EXDEV) when os.tmpdir() is on a different
431
+ * drive than the memory directory (common on Windows).
432
+ */
428
433
  private async saveToDisk(target: "memory" | "user" | "failure"): Promise<void> {
429
434
  const filePath = this.pathFor(target);
430
435
  const entries = this.entriesFor(target);
431
436
  const content = entries.length ? entries.join(ENTRY_DELIMITER) : "";
432
437
 
433
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-memory-"));
438
+ // Use the memory directory for temp files so rename stays on the same device
439
+ const tmpDir = await fs.mkdtemp(path.join(this.memoryDir, ".tmp-"));
434
440
  const tmpPath = path.join(tmpDir, "write.tmp");
435
441
 
436
442
  try {
@@ -440,7 +446,7 @@ export class MemoryStore {
440
446
  try { await fs.unlink(tmpPath); } catch { /* ignore */ }
441
447
  throw err;
442
448
  } finally {
443
- try { await fs.rmdir(tmpDir); } catch { /* ignore */ }
449
+ try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
444
450
  }
445
451
  }
446
452
  }
@@ -283,10 +283,14 @@ export class SkillStore {
283
283
 
284
284
  // ─── Internal helpers ───
285
285
 
286
- /** Atomic write: temp file + rename (same crash-safety as MemoryStore) */
286
+ /**
287
+ * Atomic write: temp file + rename.
288
+ * Creates temp files in the skills directory to avoid cross-device
289
+ * rename errors (EXDEV) on Windows when os.tmpdir() is on a different drive.
290
+ */
287
291
  private async atomicWrite(fileName: string, content: string): Promise<void> {
288
292
  const filePath = path.join(this.skillsDir, fileName);
289
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-skill-"));
293
+ const tmpDir = await fs.mkdtemp(path.join(this.skillsDir, ".tmp-"));
290
294
  const tmpPath = path.join(tmpDir, "write.tmp");
291
295
 
292
296
  try {
@@ -296,7 +300,7 @@ export class SkillStore {
296
300
  try { await fs.unlink(tmpPath); } catch { /* ignore */ }
297
301
  throw err;
298
302
  } finally {
299
- try { await fs.rmdir(tmpDir); } catch { /* ignore */ }
303
+ try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
300
304
  }
301
305
  }
302
306
  }
@@ -179,8 +179,8 @@ export function registerMemoryTool(
179
179
  async execute(toolCallId, params, signal, onUpdate, ctx) {
180
180
  const { action, target: rawTarget, content, old_text, category, failure_reason } = params;
181
181
 
182
- // Route 'project' to projectStore (internal target 'memory')
183
- const target = rawTarget as "memory" | "user" | "failure";
182
+ // Route 'project' to projectStore using the normal MEMORY.md target.
183
+ const target = rawTarget === "project" ? "memory" : rawTarget as "memory" | "user" | "failure";
184
184
  const activeStore = rawTarget === "project" ? projectStore : store;
185
185
 
186
186
  if (rawTarget === "project" && !projectStore) {
package/src/types.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  import type { TextContent } from "@mariozechner/pi-ai";
6
6
 
7
7
  export interface MemoryConfig {
8
+ /** Prompt memory mode. Default: policy-only */
9
+ memoryMode: "policy-only" | "legacy-inject";
8
10
  /** Max chars for MEMORY.md (agent notes). Default: 5000 */
9
11
  memoryCharLimit: number;
10
12
  /** Max chars for USER.md (user profile). Default: 5000 */
@@ -13,6 +15,8 @@ export interface MemoryConfig {
13
15
  projectCharLimit: number;
14
16
  /** Turns between background auto-reviews. Default: 10 */
15
17
  nudgeInterval: number;
18
+ /** Recent conversation messages included in background review. 0 = all. Default: 0 */
19
+ reviewRecentMessages?: number;
16
20
  /** Enable background learning loop. Default: true */
17
21
  reviewEnabled: boolean;
18
22
  /** Flush memories before compaction. Default: true */
@@ -21,8 +25,12 @@ export interface MemoryConfig {
21
25
  flushOnShutdown: boolean;
22
26
  /** Minimum user turns before flush triggers. Default: 6 */
23
27
  flushMinTurns: number;
28
+ /** Recent conversation messages included in session flush. 0 = all. Default: 0 */
29
+ flushRecentMessages?: number;
24
30
  /** Override memory directory. Default: ~/.pi/agent/memory */
25
31
  memoryDir?: string;
32
+ /** Directory for project-scoped memory (relative to ~/.pi/agent). Default: "projects-memory" */
33
+ projectsMemoryDir?: string;
26
34
  /** Auto-consolidate when memory is full instead of returning error. Default: true */
27
35
  autoConsolidate: boolean;
28
36
  /** Detect user corrections and trigger immediate memory save. Default: true */
@@ -1,112 +0,0 @@
1
- # v0.7 Proposal: Tagged Session Review → Skill Creation
2
-
3
- ## Why
4
-
5
- Community feedback highlighted a real gap:
6
- - quick correction capture is useful,
7
- - but **durable behavior should be promoted to skills intentionally**,
8
- - and that flow should be reviewable in the TUI.
9
-
10
- ## Goal
11
-
12
- Add a deterministic review workflow:
13
- 1. collect candidate learnings from session messages,
14
- 2. review them in a TUI modal,
15
- 3. promote selected items into a draft skill,
16
- 4. save via existing `skill` tool.
17
-
18
- ## Non-goals
19
-
20
- - Auto-creating skills without user review.
21
- - Replacing core memory entirely.
22
- - Requiring git history for extraction.
23
-
24
- ## UX (Target)
25
-
26
- ### Command
27
- - `/memory-review-candidates`
28
-
29
- ### Modal flow
30
- 1. **Candidate list** (tag, source session, snippet, confidence)
31
- 2. Actions per candidate: `approve`, `reject`, `edit`, `merge with...`
32
- 3. Multi-select candidates and choose `Create skill draft`
33
- 4. Draft editor with sections:
34
- - `## When to Use`
35
- - `## Procedure`
36
- - `## Pitfalls`
37
- - `## Verification`
38
- 5. Save with `skill.create`
39
-
40
- ## Candidate Sources
41
-
42
- Priority order:
43
- 1. Explicit message tags from Pi sessions (when available)
44
- 2. Heuristic extraction from conversation patterns:
45
- - repeated corrections,
46
- - multi-step successful runs,
47
- - repeated tool sequences,
48
- - resolved failures with clear fix.
49
-
50
- ## Data Model (SQLite)
51
-
52
- New table (proposal): `memory_candidates`
53
-
54
- Columns:
55
- - `id` INTEGER PK
56
- - `session_id` TEXT
57
- - `message_id` TEXT
58
- - `project` TEXT
59
- - `tag` TEXT
60
- - `snippet` TEXT
61
- - `rationale` TEXT
62
- - `status` TEXT CHECK (`pending`,`approved`,`rejected`,`promoted`)
63
- - `created_at` TEXT
64
- - `updated_at` TEXT
65
- - `promoted_skill` TEXT NULL
66
-
67
- ## Integration with Existing System
68
-
69
- - Keep current memory + failure capture.
70
- - Add a **promotion path** from memory/candidates to skills.
71
- - Use existing `skill` tool as persistence layer.
72
- - Use existing `session_search`/indexing infra for candidate discovery context.
73
-
74
- ## Rollout Plan
75
-
76
- ### Phase 1: Candidate staging (no modal)
77
- - Create `memory_candidates` table
78
- - Add extraction + `/memory-candidates` list command
79
- - Add approve/reject commands
80
-
81
- ### Phase 2: TUI review modal
82
- - Interactive candidate triage in one place
83
- - Batch select + merge + edit
84
-
85
- ### Phase 3: Skill draft + save
86
- - Generate skill draft from approved candidates
87
- - Edit + save with `skill.create`
88
-
89
- ### Phase 4: Quality controls
90
- - duplicate candidate suppression
91
- - confidence thresholds
92
- - weekly reminder: pending candidates review
93
-
94
- ## Success Criteria
95
-
96
- - Lower noisy memory growth in `MEMORY.md`
97
- - Higher percentage of reusable knowledge landing in `skills/`
98
- - Fewer repeated correction loops across sessions
99
- - Users report that learning feels intentional, not "whack-a-mole"
100
-
101
- ## Open Questions
102
-
103
- 1. Should candidates be extracted turn-by-turn or session-end only?
104
- 2. Should approved candidates auto-expire if not promoted in N days?
105
- 3. Should skill drafts include linked source message IDs for traceability?
106
- 4. Should project scope be default-on with optional cross-project merge mode?
107
-
108
- ## Notes
109
-
110
- This proposal complements (not replaces) core memory.
111
- Memory remains fast capture; skills remain durable procedure.
112
- The new modal creates the missing bridge between them.