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.
- package/README.md +50 -53
- package/docs/0.7/PLAN.md +349 -0
- package/docs/0.7/TASKS.md +110 -0
- package/docs/ROADMAP.md +55 -11
- package/docs/images/memory-architecture.svg +1 -1
- package/docs/images/session-lifecycle.svg +1 -1
- package/docs/mermaid/memory-architecture.mmd +15 -14
- package/docs/mermaid/session-lifecycle.mmd +7 -4
- package/package.json +2 -2
- package/src/config.ts +14 -0
- package/src/constants.ts +53 -1
- package/src/handlers/background-review.ts +5 -12
- package/src/handlers/correction-detector.ts +5 -3
- package/src/handlers/learn-memory.ts +18 -10
- package/src/handlers/message-parts.ts +27 -0
- package/src/handlers/preview-context.ts +24 -3
- package/src/handlers/session-flush.ts +2 -11
- package/src/handlers/switch-project.ts +8 -6
- package/src/handlers/sync-markdown-memories.ts +73 -41
- package/src/index.ts +23 -18
- package/src/project-memory-migration.ts +94 -0
- package/src/project.ts +3 -3
- package/src/prompt-context.ts +27 -0
- package/src/store/content-scanner.ts +1 -1
- package/src/store/memory-store.ts +9 -3
- package/src/store/skill-store.ts +7 -3
- package/src/tools/memory-tool.ts +2 -2
- package/src/types.ts +8 -0
- package/docs/0.7/TAGGED-SESSION-SKILL-REVIEW.md +0 -112
|
@@ -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
|
-
|
|
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
|
|
58
|
-
.map((name) => ({ 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
|
|
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 (
|
|
116
|
-
output += `\n📁 Project memories scanned: ${
|
|
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
|
|
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
|
|
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 (
|
|
93
|
+
if (promptContext) {
|
|
89
94
|
return {
|
|
90
|
-
systemPrompt: event.systemPrompt + "\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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
449
|
+
try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
444
450
|
}
|
|
445
451
|
}
|
|
446
452
|
}
|
package/src/store/skill-store.ts
CHANGED
|
@@ -283,10 +283,14 @@ export class SkillStore {
|
|
|
283
283
|
|
|
284
284
|
// ─── Internal helpers ───
|
|
285
285
|
|
|
286
|
-
/**
|
|
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(
|
|
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.
|
|
303
|
+
try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
300
304
|
}
|
|
301
305
|
}
|
|
302
306
|
}
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -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
|
|
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.
|