@chendpoc/pi-memory 0.1.12 → 0.1.13
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 +88 -16
- package/dist/cache/memoryCaches.d.ts +32 -0
- package/dist/cache/memoryCaches.d.ts.map +1 -0
- package/dist/cache/memoryCaches.js +59 -0
- package/dist/cache/memoryCaches.js.map +1 -0
- package/dist/cli.js +123 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +37 -1
- package/dist/config.js.map +1 -1
- package/dist/consolidation/enqueue.d.ts +33 -0
- package/dist/consolidation/enqueue.d.ts.map +1 -0
- package/dist/consolidation/enqueue.js +145 -0
- package/dist/consolidation/enqueue.js.map +1 -0
- package/dist/consolidation/index.d.ts +19 -0
- package/dist/consolidation/index.d.ts.map +1 -0
- package/dist/consolidation/index.js +15 -0
- package/dist/consolidation/index.js.map +1 -0
- package/dist/consolidation/lock.d.ts +5 -0
- package/dist/consolidation/lock.d.ts.map +1 -0
- package/dist/consolidation/lock.js +33 -0
- package/dist/consolidation/lock.js.map +1 -0
- package/dist/consolidation/log.d.ts +8 -0
- package/dist/consolidation/log.d.ts.map +1 -0
- package/dist/consolidation/log.js +32 -0
- package/dist/consolidation/log.js.map +1 -0
- package/dist/consolidation/memoryIndex.d.ts +18 -0
- package/dist/consolidation/memoryIndex.d.ts.map +1 -0
- package/dist/consolidation/memoryIndex.js +100 -0
- package/dist/consolidation/memoryIndex.js.map +1 -0
- package/dist/consolidation/phase2/runPhase2.d.ts +23 -0
- package/dist/consolidation/phase2/runPhase2.d.ts.map +1 -0
- package/dist/consolidation/phase2/runPhase2.js +196 -0
- package/dist/consolidation/phase2/runPhase2.js.map +1 -0
- package/dist/consolidation/scheduler/launchd.d.ts +5 -0
- package/dist/consolidation/scheduler/launchd.d.ts.map +1 -0
- package/dist/consolidation/scheduler/launchd.js +54 -0
- package/dist/consolidation/scheduler/launchd.js.map +1 -0
- package/dist/consolidation/scheduler/runConsolidate.d.ts +26 -0
- package/dist/consolidation/scheduler/runConsolidate.d.ts.map +1 -0
- package/dist/consolidation/scheduler/runConsolidate.js +83 -0
- package/dist/consolidation/scheduler/runConsolidate.js.map +1 -0
- package/dist/consolidation/scheduler/setupSchedule.d.ts +3 -0
- package/dist/consolidation/scheduler/setupSchedule.d.ts.map +1 -0
- package/dist/consolidation/scheduler/setupSchedule.js +79 -0
- package/dist/consolidation/scheduler/setupSchedule.js.map +1 -0
- package/dist/consolidation/scheduler/systemd.d.ts +9 -0
- package/dist/consolidation/scheduler/systemd.d.ts.map +1 -0
- package/dist/consolidation/scheduler/systemd.js +57 -0
- package/dist/consolidation/scheduler/systemd.js.map +1 -0
- package/dist/consolidation/scheduler/types.d.ts +24 -0
- package/dist/consolidation/scheduler/types.d.ts.map +1 -0
- package/dist/consolidation/scheduler/types.js +2 -0
- package/dist/consolidation/scheduler/types.js.map +1 -0
- package/dist/consolidation/scope.d.ts +12 -0
- package/dist/consolidation/scope.d.ts.map +1 -0
- package/dist/consolidation/scope.js +51 -0
- package/dist/consolidation/scope.js.map +1 -0
- package/dist/consolidation/stage1/deltaExtract.d.ts +3 -0
- package/dist/consolidation/stage1/deltaExtract.d.ts.map +1 -0
- package/dist/consolidation/stage1/deltaExtract.js +19 -0
- package/dist/consolidation/stage1/deltaExtract.js.map +1 -0
- package/dist/consolidation/stage1/drainQueue.d.ts +21 -0
- package/dist/consolidation/stage1/drainQueue.d.ts.map +1 -0
- package/dist/consolidation/stage1/drainQueue.js +113 -0
- package/dist/consolidation/stage1/drainQueue.js.map +1 -0
- package/dist/consolidation/stage1/extractSession.d.ts +6 -0
- package/dist/consolidation/stage1/extractSession.d.ts.map +1 -0
- package/dist/consolidation/stage1/extractSession.js +47 -0
- package/dist/consolidation/stage1/extractSession.js.map +1 -0
- package/dist/consolidation/stage1/store.d.ts +45 -0
- package/dist/consolidation/stage1/store.d.ts.map +1 -0
- package/dist/consolidation/stage1/store.js +297 -0
- package/dist/consolidation/stage1/store.js.map +1 -0
- package/dist/consolidation/types.d.ts +49 -0
- package/dist/consolidation/types.d.ts.map +1 -0
- package/dist/consolidation/types.js +2 -0
- package/dist/consolidation/types.js.map +1 -0
- package/dist/fallback/llmRerank.d.ts.map +1 -1
- package/dist/fallback/llmRerank.js +9 -1
- package/dist/fallback/llmRerank.js.map +1 -1
- package/dist/fallback/sessionIndex.d.ts.map +1 -1
- package/dist/fallback/sessionIndex.js +15 -63
- package/dist/fallback/sessionIndex.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/local/graphQuery.d.ts +10 -0
- package/dist/local/graphQuery.d.ts.map +1 -1
- package/dist/local/graphQuery.js +25 -0
- package/dist/local/graphQuery.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +356 -22
- package/dist/pi-extension.js.map +1 -1
- package/dist/preflight/detectIntents.d.ts.map +1 -1
- package/dist/preflight/detectIntents.js +7 -0
- package/dist/preflight/detectIntents.js.map +1 -1
- package/dist/preflight/hook.d.ts +18 -0
- package/dist/preflight/hook.d.ts.map +1 -1
- package/dist/preflight/hook.js +52 -7
- package/dist/preflight/hook.js.map +1 -1
- package/dist/preflight/render.d.ts +16 -2
- package/dist/preflight/render.d.ts.map +1 -1
- package/dist/preflight/render.js +24 -4
- package/dist/preflight/render.js.map +1 -1
- package/dist/service.d.ts +16 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +57 -0
- package/dist/service.js.map +1 -1
- package/dist/session/activeBranch.d.ts +17 -0
- package/dist/session/activeBranch.d.ts.map +1 -0
- package/dist/session/activeBranch.js +251 -0
- package/dist/session/activeBranch.js.map +1 -0
- package/dist/settings.d.ts +5 -2
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +18 -1
- package/dist/settings.js.map +1 -1
- package/dist/tools/memoryAppend.d.ts +14 -4
- package/dist/tools/memoryAppend.d.ts.map +1 -1
- package/dist/tools/memoryAppend.js +46 -7
- package/dist/tools/memoryAppend.js.map +1 -1
- package/dist/tools/memoryRecall.d.ts +2 -2
- package/dist/tools/memoryRecall.d.ts.map +1 -1
- package/dist/tools/memoryRecall.js +35 -9
- package/dist/tools/memoryRecall.js.map +1 -1
- package/dist/trainer/extractFacts.d.ts +1 -1
- package/dist/trainer/index.d.ts +1 -1
- package/dist/trainer/index.d.ts.map +1 -1
- package/dist/trainer/index.js +1 -1
- package/dist/trainer/index.js.map +1 -1
- package/dist/trainer/scheduler.d.ts +2 -0
- package/dist/trainer/scheduler.d.ts.map +1 -1
- package/dist/trainer/scheduler.js +1 -0
- package/dist/trainer/scheduler.js.map +1 -1
- package/dist/trainer/sessionLoader.d.ts +3 -0
- package/dist/trainer/sessionLoader.d.ts.map +1 -1
- package/dist/trainer/sessionLoader.js +37 -74
- package/dist/trainer/sessionLoader.js.map +1 -1
- package/package.json +14 -4
- package/src/cli.ts +144 -0
- package/src/config.ts +89 -1
- package/src/consolidation/enqueue.ts +205 -0
- package/src/consolidation/index.ts +90 -0
- package/src/consolidation/lock.ts +41 -0
- package/src/consolidation/log.ts +43 -0
- package/src/consolidation/memoryIndex.ts +127 -0
- package/src/consolidation/phase2/runPhase2.ts +255 -0
- package/src/consolidation/scheduler/launchd.ts +62 -0
- package/src/consolidation/scheduler/runConsolidate.ts +115 -0
- package/src/consolidation/scheduler/setupSchedule.ts +106 -0
- package/src/consolidation/scheduler/systemd.ts +78 -0
- package/src/consolidation/scheduler/types.ts +27 -0
- package/src/consolidation/scope.ts +58 -0
- package/src/consolidation/stage1/deltaExtract.ts +29 -0
- package/src/consolidation/stage1/drainQueue.ts +143 -0
- package/src/consolidation/stage1/extractSession.ts +58 -0
- package/src/consolidation/stage1/store.ts +411 -0
- package/src/consolidation/types.ts +52 -0
- package/src/fallback/sessionIndex.ts +14 -51
- package/src/index.ts +6 -0
- package/src/pi-extension.ts +309 -10
- package/src/preflight/hook.ts +16 -2
- package/src/preflight/render.ts +7 -0
- package/src/session/activeBranch.ts +294 -0
- package/src/settings.ts +37 -2
- package/src/tools/memoryAppend.ts +60 -7
- package/src/trainer/index.ts +1 -1
- package/src/trainer/sessionLoader.ts +38 -87
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ConsolidationStore } from "../stage1/store.js";
|
|
5
|
+
import type { Stage1Output } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export interface Phase2Options {
|
|
8
|
+
memoryMdPath: string;
|
|
9
|
+
bundleRoot: string;
|
|
10
|
+
topN: number;
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
maxLines?: number;
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
maxUnusedDays?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Phase2Report {
|
|
18
|
+
stage1Selected: number;
|
|
19
|
+
appended: number;
|
|
20
|
+
skippedDedup: number;
|
|
21
|
+
backupsCreated: number;
|
|
22
|
+
topicsUpdated: number;
|
|
23
|
+
migratedProjectLines: number;
|
|
24
|
+
prunedLines: number;
|
|
25
|
+
dryRun: boolean;
|
|
26
|
+
targets: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readFileOptional(filePath: string): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
return await fs.readFile(filePath, "utf8");
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripScopeComment(line: string): string {
|
|
38
|
+
return line.replace(/<!--\s*scope:[^>]+-->/g, "").trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizedLine(line: string): string {
|
|
42
|
+
return stripScopeComment(line).replace(/\s+/g, " ").toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function targetForRow(row: Stage1Output, opts: Phase2Options): string {
|
|
46
|
+
if (row.scope.startsWith("project:")) {
|
|
47
|
+
const hash = row.scope.slice("project:".length);
|
|
48
|
+
return path.join(opts.bundleRoot, "projects", hash, "MEMORY.md");
|
|
49
|
+
}
|
|
50
|
+
return opts.memoryMdPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function targetForScope(scope: string, opts: Phase2Options): string {
|
|
54
|
+
if (scope.startsWith("project:")) {
|
|
55
|
+
const hash = scope.slice("project:".length);
|
|
56
|
+
return path.join(opts.bundleRoot, "projects", hash, "MEMORY.md");
|
|
57
|
+
}
|
|
58
|
+
return opts.memoryMdPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rowsToMemoryLines(row: Stage1Output): string[] {
|
|
62
|
+
const body = row.raw_memory.trim() || row.rollout_summary.trim();
|
|
63
|
+
if (!body) return [];
|
|
64
|
+
return body
|
|
65
|
+
.split("\n")
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((line) => {
|
|
69
|
+
const bullet = line.startsWith("- ") ? line : `- ${line}`;
|
|
70
|
+
return `${bullet} <!-- scope:${row.scope} -->`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function backupMemoryFile(filePath: string): Promise<boolean> {
|
|
75
|
+
const existing = await readFileOptional(filePath);
|
|
76
|
+
if (!existing) return false;
|
|
77
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
78
|
+
await fs.copyFile(filePath, `${filePath}.${stamp}.bak`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function appendDedup(
|
|
83
|
+
filePath: string,
|
|
84
|
+
lines: string[],
|
|
85
|
+
dryRun: boolean,
|
|
86
|
+
): Promise<{ appended: number; skipped: number; backup: boolean }> {
|
|
87
|
+
if (lines.length === 0) return { appended: 0, skipped: 0, backup: false };
|
|
88
|
+
const existing = await readFileOptional(filePath);
|
|
89
|
+
const seen = new Set(
|
|
90
|
+
existing
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map(normalizedLine)
|
|
93
|
+
.filter(Boolean),
|
|
94
|
+
);
|
|
95
|
+
const nextLines: string[] = [];
|
|
96
|
+
let skipped = 0;
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const key = normalizedLine(line);
|
|
99
|
+
if (seen.has(key)) {
|
|
100
|
+
skipped++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
seen.add(key);
|
|
104
|
+
nextLines.push(line);
|
|
105
|
+
}
|
|
106
|
+
if (dryRun || nextLines.length === 0) {
|
|
107
|
+
return { appended: nextLines.length, skipped, backup: false };
|
|
108
|
+
}
|
|
109
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
110
|
+
const backup = await backupMemoryFile(filePath);
|
|
111
|
+
const prefix = existing.trim() ? "\n" : "# Memory\n\n";
|
|
112
|
+
await fs.appendFile(filePath, `${prefix}${nextLines.join("\n")}\n`, {
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
mode: 0o600,
|
|
115
|
+
});
|
|
116
|
+
return { appended: nextLines.length, skipped, backup };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function migrateProjectScopedLines(
|
|
120
|
+
opts: Phase2Options,
|
|
121
|
+
): Promise<{ migrated: number; targets: string[] }> {
|
|
122
|
+
const globalText = await readFileOptional(opts.memoryMdPath);
|
|
123
|
+
if (!globalText.trim()) return { migrated: 0, targets: [] };
|
|
124
|
+
|
|
125
|
+
const keep: string[] = [];
|
|
126
|
+
const byTarget = new Map<string, string[]>();
|
|
127
|
+
for (const line of globalText.split("\n")) {
|
|
128
|
+
const scope = line.match(/<!--\s*scope:(project:[a-f0-9]{12})\s*-->/i)?.[1];
|
|
129
|
+
if (!scope) {
|
|
130
|
+
keep.push(line);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const target = targetForScope(scope, opts);
|
|
134
|
+
byTarget.set(target, [...(byTarget.get(target) ?? []), line]);
|
|
135
|
+
}
|
|
136
|
+
const migrated = Array.from(byTarget.values()).reduce((sum, lines) => sum + lines.length, 0);
|
|
137
|
+
if (opts.dryRun || migrated === 0) {
|
|
138
|
+
return { migrated, targets: Array.from(byTarget.keys()) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await backupMemoryFile(opts.memoryMdPath);
|
|
142
|
+
await fs.writeFile(opts.memoryMdPath, `${keep.join("\n").trim()}\n`, "utf8");
|
|
143
|
+
for (const [target, lines] of byTarget) {
|
|
144
|
+
await appendDedup(target, lines, false);
|
|
145
|
+
}
|
|
146
|
+
return { migrated, targets: Array.from(byTarget.keys()) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function pruneLinesFromTargets(
|
|
150
|
+
rows: Stage1Output[],
|
|
151
|
+
opts: Phase2Options,
|
|
152
|
+
): Promise<number> {
|
|
153
|
+
if (rows.length === 0) return 0;
|
|
154
|
+
const staleKeys = new Set<string>();
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
for (const line of rowsToMemoryLines(row)) staleKeys.add(normalizedLine(line));
|
|
157
|
+
}
|
|
158
|
+
if (staleKeys.size === 0) return 0;
|
|
159
|
+
|
|
160
|
+
const targets = new Set<string>([
|
|
161
|
+
opts.memoryMdPath,
|
|
162
|
+
...rows.map((row) => targetForRow(row, opts)),
|
|
163
|
+
]);
|
|
164
|
+
let pruned = 0;
|
|
165
|
+
for (const target of targets) {
|
|
166
|
+
const existing = await readFileOptional(target);
|
|
167
|
+
if (!existing.trim()) continue;
|
|
168
|
+
const kept: string[] = [];
|
|
169
|
+
let targetPruned = 0;
|
|
170
|
+
for (const line of existing.split("\n")) {
|
|
171
|
+
if (staleKeys.has(normalizedLine(line))) {
|
|
172
|
+
pruned++;
|
|
173
|
+
targetPruned++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
kept.push(line);
|
|
177
|
+
}
|
|
178
|
+
if (!opts.dryRun && targetPruned > 0) {
|
|
179
|
+
await backupMemoryFile(target);
|
|
180
|
+
await fs.writeFile(target, `${kept.join("\n").trim()}\n`, "utf8");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return pruned;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function syncWorkspace(
|
|
187
|
+
rows: Stage1Output[],
|
|
188
|
+
bundleRoot: string,
|
|
189
|
+
dryRun: boolean,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
if (dryRun) return;
|
|
192
|
+
const workspace = path.join(bundleRoot, "workspace");
|
|
193
|
+
const summaries = path.join(workspace, "rollout_summaries");
|
|
194
|
+
await fs.mkdir(summaries, { recursive: true, mode: 0o700 });
|
|
195
|
+
const raw = rows
|
|
196
|
+
.map((row) => `## ${row.session_id}\n\n${row.raw_memory || row.rollout_summary}`)
|
|
197
|
+
.join("\n\n");
|
|
198
|
+
await fs.writeFile(path.join(workspace, "raw_memories.md"), `${raw}\n`, "utf8");
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
await fs.writeFile(
|
|
201
|
+
path.join(summaries, `${row.session_id}.md`),
|
|
202
|
+
`${row.rollout_summary || row.raw_memory}\n`,
|
|
203
|
+
"utf8",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function runPhase2(
|
|
209
|
+
store: ConsolidationStore,
|
|
210
|
+
opts: Phase2Options,
|
|
211
|
+
): Promise<Phase2Report> {
|
|
212
|
+
const dryRun = opts.dryRun ?? false;
|
|
213
|
+
const rows = store.listUnselectedStage1(opts.topN);
|
|
214
|
+
const migration = await migrateProjectScopedLines(opts);
|
|
215
|
+
const grouped = new Map<string, string[]>();
|
|
216
|
+
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const target = targetForRow(row, opts);
|
|
219
|
+
const lines = rowsToMemoryLines(row);
|
|
220
|
+
grouped.set(target, [...(grouped.get(target) ?? []), ...lines]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const report: Phase2Report = {
|
|
224
|
+
stage1Selected: rows.length,
|
|
225
|
+
appended: 0,
|
|
226
|
+
skippedDedup: 0,
|
|
227
|
+
backupsCreated: 0,
|
|
228
|
+
topicsUpdated: 0,
|
|
229
|
+
migratedProjectLines: migration.migrated,
|
|
230
|
+
prunedLines: 0,
|
|
231
|
+
dryRun,
|
|
232
|
+
targets: Array.from(new Set([...grouped.keys(), ...migration.targets])),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const [target, lines] of grouped) {
|
|
236
|
+
const result = await appendDedup(target, lines, dryRun);
|
|
237
|
+
report.appended += result.appended;
|
|
238
|
+
report.skippedDedup += result.skipped;
|
|
239
|
+
if (result.backup) report.backupsCreated++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await syncWorkspace(rows, opts.bundleRoot, dryRun);
|
|
243
|
+
if (opts.maxUnusedDays && opts.maxUnusedDays > 0) {
|
|
244
|
+
const cutoff = new Date(
|
|
245
|
+
Date.now() - opts.maxUnusedDays * 24 * 60 * 60 * 1000,
|
|
246
|
+
).toISOString();
|
|
247
|
+
report.prunedLines = await pruneLinesFromTargets(
|
|
248
|
+
store.listStage1OlderThan(cutoff),
|
|
249
|
+
opts,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (!dryRun) store.markStage1Selected(rows.map((row) => row.session_id));
|
|
253
|
+
|
|
254
|
+
return report;
|
|
255
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ScheduleOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const LAUNCHD_LABEL = "dev.pi.memory-consolidate";
|
|
7
|
+
const PLIST_FILE = `${LAUNCHD_LABEL}.plist`;
|
|
8
|
+
|
|
9
|
+
function commandArgs(opts: ScheduleOptions): string[] {
|
|
10
|
+
if (opts.commandPath) {
|
|
11
|
+
return [opts.commandPath, "consolidate"];
|
|
12
|
+
}
|
|
13
|
+
if (opts.npxPath) {
|
|
14
|
+
return [opts.npxPath, "pi-memory", "consolidate"];
|
|
15
|
+
}
|
|
16
|
+
return ["pi-memory", "consolidate"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function escapeXml(value: string): string {
|
|
20
|
+
return value
|
|
21
|
+
.replaceAll("&", "&")
|
|
22
|
+
.replaceAll("<", "<")
|
|
23
|
+
.replaceAll(">", ">");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function launchdPlistPath(): string {
|
|
27
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", PLIST_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildLaunchdPlist(opts: ScheduleOptions): string {
|
|
31
|
+
const args = commandArgs(opts);
|
|
32
|
+
|
|
33
|
+
const argumentsXml = args
|
|
34
|
+
.map((arg) => ` <string>${escapeXml(arg)}</string>`)
|
|
35
|
+
.join("\n");
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
39
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
40
|
+
`<plist version="1.0">`,
|
|
41
|
+
`<dict>`,
|
|
42
|
+
` <key>Label</key>`,
|
|
43
|
+
` <string>${LAUNCHD_LABEL}</string>`,
|
|
44
|
+
` <key>ProgramArguments</key>`,
|
|
45
|
+
` <array>`,
|
|
46
|
+
`${argumentsXml}`,
|
|
47
|
+
` </array>`,
|
|
48
|
+
` <key>StandardOutPath</key>`,
|
|
49
|
+
` <string>${escapeXml(opts.logPath)}</string>`,
|
|
50
|
+
` <key>StandardErrorPath</key>`,
|
|
51
|
+
` <string>${escapeXml(opts.logPath)}</string>`,
|
|
52
|
+
` <key>StartCalendarInterval</key>`,
|
|
53
|
+
` <dict>`,
|
|
54
|
+
` <key>Hour</key>`,
|
|
55
|
+
` <integer>${opts.hour}</integer>`,
|
|
56
|
+
` <key>Minute</key>`,
|
|
57
|
+
` <integer>${opts.minute}</integer>`,
|
|
58
|
+
` </dict>`,
|
|
59
|
+
`</dict>`,
|
|
60
|
+
`</plist>`,
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { MemoryConfig } from "../../config.js";
|
|
4
|
+
import { trainBundle } from "../../trainer/index.js";
|
|
5
|
+
import { withFileLock } from "../lock.js";
|
|
6
|
+
import { appendConsolidationLog } from "../log.js";
|
|
7
|
+
import { runPhase2, type Phase2Report } from "../phase2/runPhase2.js";
|
|
8
|
+
import { drainQueue, type DrainQueueReport } from "../stage1/drainQueue.js";
|
|
9
|
+
import { openConsolidationStore } from "../stage1/store.js";
|
|
10
|
+
|
|
11
|
+
export interface ConsolidateOptions {
|
|
12
|
+
config: MemoryConfig;
|
|
13
|
+
dbPath?: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
phase1Only?: boolean;
|
|
16
|
+
phase2Only?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConsolidateReport {
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
dbPath: string;
|
|
22
|
+
drain?: DrainQueueReport;
|
|
23
|
+
train?: {
|
|
24
|
+
sessionsProcessed: number;
|
|
25
|
+
entityCount: number;
|
|
26
|
+
relationCount: number;
|
|
27
|
+
eventCount: number;
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
};
|
|
30
|
+
phase2?: Phase2Report;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function defaultConsolidationDbPath(cfg: MemoryConfig): string {
|
|
34
|
+
return path.join(cfg.bundleRoot, "memories.sqlite");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runConsolidate(
|
|
38
|
+
opts: ConsolidateOptions,
|
|
39
|
+
): Promise<ConsolidateReport> {
|
|
40
|
+
const cfg = opts.config;
|
|
41
|
+
const dbPath = opts.dbPath ?? defaultConsolidationDbPath(cfg);
|
|
42
|
+
const dryRun = opts.dryRun ?? false;
|
|
43
|
+
const lockPath = path.join(cfg.bundleRoot, ".consolidation.lock");
|
|
44
|
+
const logPath = cfg.consolidation.schedule.log_path;
|
|
45
|
+
|
|
46
|
+
return withFileLock(lockPath, async () => {
|
|
47
|
+
const store = openConsolidationStore(dbPath);
|
|
48
|
+
if (!store) {
|
|
49
|
+
throw new Error("better-sqlite3 unavailable; cannot open consolidation store");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const report: ConsolidateReport = { dryRun, dbPath };
|
|
54
|
+
|
|
55
|
+
if (!opts.phase2Only) {
|
|
56
|
+
report.drain = await drainQueue(store, { dryRun });
|
|
57
|
+
await appendConsolidationLog(logPath, {
|
|
58
|
+
phase: "phase1",
|
|
59
|
+
sessions_processed: report.drain.sessionsProcessed,
|
|
60
|
+
sessions_skipped: report.drain.sessionsSkipped,
|
|
61
|
+
sessions_failed: report.drain.sessionsFailed,
|
|
62
|
+
stage1_new_rows: report.drain.stage1NewRows,
|
|
63
|
+
dry_run: dryRun,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const trainResult = await trainBundle({
|
|
67
|
+
sessionsDir: cfg.sessionsDir,
|
|
68
|
+
bundleRoot: cfg.bundleRoot,
|
|
69
|
+
dryRun,
|
|
70
|
+
full: false,
|
|
71
|
+
});
|
|
72
|
+
report.train = {
|
|
73
|
+
sessionsProcessed: trainResult.sessionsProcessed,
|
|
74
|
+
entityCount: trainResult.entityCount,
|
|
75
|
+
relationCount: trainResult.relationCount,
|
|
76
|
+
eventCount: trainResult.eventCount,
|
|
77
|
+
dryRun: trainResult.dryRun,
|
|
78
|
+
};
|
|
79
|
+
await appendConsolidationLog(logPath, {
|
|
80
|
+
phase: "graph_train",
|
|
81
|
+
sessions_processed: trainResult.sessionsProcessed,
|
|
82
|
+
entities: trainResult.entityCount,
|
|
83
|
+
relations: trainResult.relationCount,
|
|
84
|
+
events: trainResult.eventCount,
|
|
85
|
+
dry_run: dryRun,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!opts.phase1Only) {
|
|
90
|
+
const memoryMdPath = cfg.memoryMdPaths[0] ?? path.join(cfg.bundleRoot, "MEMORY.md");
|
|
91
|
+
report.phase2 = await runPhase2(store, {
|
|
92
|
+
memoryMdPath,
|
|
93
|
+
bundleRoot: cfg.bundleRoot,
|
|
94
|
+
topN: cfg.consolidation.phase2_top_n,
|
|
95
|
+
dryRun,
|
|
96
|
+
maxLines: cfg.consolidation.memory_index_max_lines,
|
|
97
|
+
maxBytes: cfg.consolidation.memory_index_max_bytes,
|
|
98
|
+
maxUnusedDays: cfg.consolidation.max_unused_days,
|
|
99
|
+
});
|
|
100
|
+
await appendConsolidationLog(logPath, {
|
|
101
|
+
phase: "phase2",
|
|
102
|
+
stage1_selected: report.phase2.stage1Selected,
|
|
103
|
+
appended: report.phase2.appended,
|
|
104
|
+
skipped_dedup: report.phase2.skippedDedup,
|
|
105
|
+
backups_created: report.phase2.backupsCreated,
|
|
106
|
+
dry_run: dryRun,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return report;
|
|
111
|
+
} finally {
|
|
112
|
+
store.close();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { buildLaunchdPlist, launchdPlistPath } from "./launchd.js";
|
|
5
|
+
import {
|
|
6
|
+
buildSystemdService,
|
|
7
|
+
buildSystemdTimer,
|
|
8
|
+
systemdServicePath,
|
|
9
|
+
systemdTimerPath,
|
|
10
|
+
} from "./systemd.js";
|
|
11
|
+
import {
|
|
12
|
+
type SetupScheduleResult,
|
|
13
|
+
type ScheduledFile,
|
|
14
|
+
type ScheduleAction,
|
|
15
|
+
type ScheduleOptions,
|
|
16
|
+
type SchedulePlatform,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
|
|
19
|
+
async function exists(filePath: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(filePath);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureDir(filePath: string): Promise<void> {
|
|
29
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeIfNeeded(
|
|
33
|
+
filePath: string,
|
|
34
|
+
content: string,
|
|
35
|
+
dryRun: boolean,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
if (dryRun) return;
|
|
38
|
+
await ensureDir(filePath);
|
|
39
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function removeFileIfExists(filePath: string, dryRun: boolean): Promise<void> {
|
|
43
|
+
if (dryRun) return;
|
|
44
|
+
try {
|
|
45
|
+
await fs.rm(filePath, { force: true });
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore cleanup failures for idempotency.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setupPlan(
|
|
52
|
+
platform: SchedulePlatform,
|
|
53
|
+
opts: ScheduleOptions,
|
|
54
|
+
): Array<{ path: string; content: string }> {
|
|
55
|
+
if (platform === "darwin") {
|
|
56
|
+
return [{ path: launchdPlistPath(), content: buildLaunchdPlist(opts) }];
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
{ path: systemdServicePath(), content: buildSystemdService(opts) },
|
|
60
|
+
{ path: systemdTimerPath(), content: buildSystemdTimer(opts) },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveAction(opts: ScheduleOptions): ScheduleAction {
|
|
65
|
+
if (opts.status) return "status";
|
|
66
|
+
if (opts.remove) return "remove";
|
|
67
|
+
return "write";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function setupSchedule(
|
|
71
|
+
opts: ScheduleOptions,
|
|
72
|
+
platform: SchedulePlatform,
|
|
73
|
+
): Promise<SetupScheduleResult> {
|
|
74
|
+
const dryRun = Boolean(opts.dryRun);
|
|
75
|
+
const action = resolveAction(opts);
|
|
76
|
+
const filesToProcess = setupPlan(platform, opts);
|
|
77
|
+
|
|
78
|
+
const files: ScheduledFile[] = [];
|
|
79
|
+
|
|
80
|
+
if (action === "status") {
|
|
81
|
+
for (const file of filesToProcess) {
|
|
82
|
+
files.push({ path: file.path, exists: await exists(file.path) });
|
|
83
|
+
}
|
|
84
|
+
return { platform, action, files, dryRun };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (action === "remove") {
|
|
88
|
+
for (const file of filesToProcess) {
|
|
89
|
+
await removeFileIfExists(file.path, dryRun);
|
|
90
|
+
}
|
|
91
|
+
for (const file of filesToProcess) {
|
|
92
|
+
files.push({ path: file.path, exists: await exists(file.path) });
|
|
93
|
+
}
|
|
94
|
+
return { platform, action, files, dryRun };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const file of filesToProcess) {
|
|
98
|
+
await writeIfNeeded(file.path, file.content, dryRun);
|
|
99
|
+
files.push({
|
|
100
|
+
path: file.path,
|
|
101
|
+
exists: await exists(file.path),
|
|
102
|
+
content: file.content,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { platform, action, files, dryRun };
|
|
106
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ScheduleOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const SYSTEMD_UNIT_NAME = "dev.pi.memory-consolidate";
|
|
7
|
+
export const SYSTEMD_SERVICE_FILE = `${SYSTEMD_UNIT_NAME}.service`;
|
|
8
|
+
export const SYSTEMD_TIMER_FILE = `${SYSTEMD_UNIT_NAME}.timer`;
|
|
9
|
+
|
|
10
|
+
function commandArgs(opts: ScheduleOptions): string[] {
|
|
11
|
+
if (opts.commandPath) {
|
|
12
|
+
return [opts.commandPath, "consolidate"];
|
|
13
|
+
}
|
|
14
|
+
if (opts.npxPath) {
|
|
15
|
+
return [opts.npxPath, "pi-memory", "consolidate"];
|
|
16
|
+
}
|
|
17
|
+
return ["pi-memory", "consolidate"];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pad2(value: number, max: number): string {
|
|
21
|
+
return String(Math.max(0, Math.min(max, Math.trunc(value)))).padStart(2, "0");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildExecStart(opts: ScheduleOptions): string {
|
|
25
|
+
return commandArgs(opts).join(" ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function systemdServicePath(): string {
|
|
29
|
+
return path.join(
|
|
30
|
+
os.homedir(),
|
|
31
|
+
".config",
|
|
32
|
+
"systemd",
|
|
33
|
+
"user",
|
|
34
|
+
SYSTEMD_SERVICE_FILE,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function systemdTimerPath(): string {
|
|
39
|
+
return path.join(
|
|
40
|
+
os.homedir(),
|
|
41
|
+
".config",
|
|
42
|
+
"systemd",
|
|
43
|
+
"user",
|
|
44
|
+
SYSTEMD_TIMER_FILE,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildSystemdService(opts: ScheduleOptions): string {
|
|
49
|
+
return [
|
|
50
|
+
`[Unit]`,
|
|
51
|
+
`Description=Pi Memory Consolidation`,
|
|
52
|
+
``,
|
|
53
|
+
`[Service]`,
|
|
54
|
+
`Type=oneshot`,
|
|
55
|
+
`ExecStart=${buildExecStart(opts)}`,
|
|
56
|
+
`StandardOutput=append:${opts.logPath}`,
|
|
57
|
+
`StandardError=append:${opts.logPath}`,
|
|
58
|
+
``,
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildSystemdTimer(opts: ScheduleOptions): string {
|
|
63
|
+
const minute = pad2(opts.minute, 59);
|
|
64
|
+
const hour = pad2(opts.hour, 23);
|
|
65
|
+
return [
|
|
66
|
+
`[Unit]`,
|
|
67
|
+
`Description=Runs Pi memory consolidation on a daily schedule`,
|
|
68
|
+
``,
|
|
69
|
+
`[Timer]`,
|
|
70
|
+
`Persistent=true`,
|
|
71
|
+
`OnCalendar=*-*-* ${hour}:${minute}:00`,
|
|
72
|
+
`Unit=${SYSTEMD_UNIT_NAME}.service`,
|
|
73
|
+
``,
|
|
74
|
+
`[Install]`,
|
|
75
|
+
`WantedBy=timers.target`,
|
|
76
|
+
``,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SchedulePlatform = "darwin" | "linux";
|
|
2
|
+
|
|
3
|
+
export interface ScheduleOptions {
|
|
4
|
+
hour: number;
|
|
5
|
+
minute: number;
|
|
6
|
+
logPath: string;
|
|
7
|
+
commandPath?: string;
|
|
8
|
+
npxPath?: string;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
remove?: boolean;
|
|
11
|
+
status?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ScheduleAction = "write" | "remove" | "status";
|
|
15
|
+
|
|
16
|
+
export interface ScheduledFile {
|
|
17
|
+
path: string;
|
|
18
|
+
exists: boolean;
|
|
19
|
+
content?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SetupScheduleResult {
|
|
23
|
+
platform: SchedulePlatform;
|
|
24
|
+
action: ScheduleAction;
|
|
25
|
+
files: ScheduledFile[];
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type MemoryScope = "global" | `project:${string}`;
|
|
6
|
+
|
|
7
|
+
const SCOPE_RE = /<!--\s*scope:([^>\s]+)\s*-->/;
|
|
8
|
+
|
|
9
|
+
export function projectHash(input: string): string {
|
|
10
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function projectScope(projectHashValue: string | null | undefined): MemoryScope {
|
|
14
|
+
return projectHashValue ? `project:${projectHashValue}` : "global";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseScopeComment(line: string): MemoryScope | null {
|
|
18
|
+
const raw = line.match(SCOPE_RE)?.[1]?.trim();
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
if (raw === "global") return "global";
|
|
21
|
+
if (/^project:[a-f0-9]{12}$/i.test(raw)) return raw as MemoryScope;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function lineMatchesScope(
|
|
26
|
+
line: string,
|
|
27
|
+
allowedScopes: readonly MemoryScope[] | undefined,
|
|
28
|
+
): boolean {
|
|
29
|
+
if (!allowedScopes || allowedScopes.length === 0) return true;
|
|
30
|
+
const scope = parseScopeComment(line);
|
|
31
|
+
if (!scope) return true;
|
|
32
|
+
return allowedScopes.includes(scope);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findGitRoot(startDir: string | null | undefined): string | null {
|
|
36
|
+
if (!startDir) return null;
|
|
37
|
+
let current = path.resolve(startDir);
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
40
|
+
const parent = path.dirname(current);
|
|
41
|
+
if (parent === current) return null;
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function scopeForCwd(cwd: string | null | undefined): {
|
|
47
|
+
gitRoot: string | null;
|
|
48
|
+
projectHash: string | null;
|
|
49
|
+
scopes: MemoryScope[];
|
|
50
|
+
} {
|
|
51
|
+
const gitRoot = findGitRoot(cwd);
|
|
52
|
+
const hash = gitRoot ? projectHash(gitRoot) : null;
|
|
53
|
+
return {
|
|
54
|
+
gitRoot,
|
|
55
|
+
projectHash: hash,
|
|
56
|
+
scopes: hash ? ["global", `project:${hash}`] : ["global"],
|
|
57
|
+
};
|
|
58
|
+
}
|