@chendpoc/pi-memory 0.1.11 → 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.
Files changed (177) hide show
  1. package/README.md +88 -16
  2. package/dist/cache/memoryCaches.d.ts +32 -0
  3. package/dist/cache/memoryCaches.d.ts.map +1 -0
  4. package/dist/cache/memoryCaches.js +59 -0
  5. package/dist/cache/memoryCaches.js.map +1 -0
  6. package/dist/cli.js +123 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +28 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +37 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/consolidation/enqueue.d.ts +33 -0
  13. package/dist/consolidation/enqueue.d.ts.map +1 -0
  14. package/dist/consolidation/enqueue.js +145 -0
  15. package/dist/consolidation/enqueue.js.map +1 -0
  16. package/dist/consolidation/index.d.ts +19 -0
  17. package/dist/consolidation/index.d.ts.map +1 -0
  18. package/dist/consolidation/index.js +15 -0
  19. package/dist/consolidation/index.js.map +1 -0
  20. package/dist/consolidation/lock.d.ts +5 -0
  21. package/dist/consolidation/lock.d.ts.map +1 -0
  22. package/dist/consolidation/lock.js +33 -0
  23. package/dist/consolidation/lock.js.map +1 -0
  24. package/dist/consolidation/log.d.ts +8 -0
  25. package/dist/consolidation/log.d.ts.map +1 -0
  26. package/dist/consolidation/log.js +32 -0
  27. package/dist/consolidation/log.js.map +1 -0
  28. package/dist/consolidation/memoryIndex.d.ts +18 -0
  29. package/dist/consolidation/memoryIndex.d.ts.map +1 -0
  30. package/dist/consolidation/memoryIndex.js +100 -0
  31. package/dist/consolidation/memoryIndex.js.map +1 -0
  32. package/dist/consolidation/phase2/runPhase2.d.ts +23 -0
  33. package/dist/consolidation/phase2/runPhase2.d.ts.map +1 -0
  34. package/dist/consolidation/phase2/runPhase2.js +196 -0
  35. package/dist/consolidation/phase2/runPhase2.js.map +1 -0
  36. package/dist/consolidation/scheduler/launchd.d.ts +5 -0
  37. package/dist/consolidation/scheduler/launchd.d.ts.map +1 -0
  38. package/dist/consolidation/scheduler/launchd.js +54 -0
  39. package/dist/consolidation/scheduler/launchd.js.map +1 -0
  40. package/dist/consolidation/scheduler/runConsolidate.d.ts +26 -0
  41. package/dist/consolidation/scheduler/runConsolidate.d.ts.map +1 -0
  42. package/dist/consolidation/scheduler/runConsolidate.js +83 -0
  43. package/dist/consolidation/scheduler/runConsolidate.js.map +1 -0
  44. package/dist/consolidation/scheduler/setupSchedule.d.ts +3 -0
  45. package/dist/consolidation/scheduler/setupSchedule.d.ts.map +1 -0
  46. package/dist/consolidation/scheduler/setupSchedule.js +79 -0
  47. package/dist/consolidation/scheduler/setupSchedule.js.map +1 -0
  48. package/dist/consolidation/scheduler/systemd.d.ts +9 -0
  49. package/dist/consolidation/scheduler/systemd.d.ts.map +1 -0
  50. package/dist/consolidation/scheduler/systemd.js +57 -0
  51. package/dist/consolidation/scheduler/systemd.js.map +1 -0
  52. package/dist/consolidation/scheduler/types.d.ts +24 -0
  53. package/dist/consolidation/scheduler/types.d.ts.map +1 -0
  54. package/dist/consolidation/scheduler/types.js +2 -0
  55. package/dist/consolidation/scheduler/types.js.map +1 -0
  56. package/dist/consolidation/scope.d.ts +12 -0
  57. package/dist/consolidation/scope.d.ts.map +1 -0
  58. package/dist/consolidation/scope.js +51 -0
  59. package/dist/consolidation/scope.js.map +1 -0
  60. package/dist/consolidation/stage1/deltaExtract.d.ts +3 -0
  61. package/dist/consolidation/stage1/deltaExtract.d.ts.map +1 -0
  62. package/dist/consolidation/stage1/deltaExtract.js +19 -0
  63. package/dist/consolidation/stage1/deltaExtract.js.map +1 -0
  64. package/dist/consolidation/stage1/drainQueue.d.ts +21 -0
  65. package/dist/consolidation/stage1/drainQueue.d.ts.map +1 -0
  66. package/dist/consolidation/stage1/drainQueue.js +113 -0
  67. package/dist/consolidation/stage1/drainQueue.js.map +1 -0
  68. package/dist/consolidation/stage1/extractSession.d.ts +6 -0
  69. package/dist/consolidation/stage1/extractSession.d.ts.map +1 -0
  70. package/dist/consolidation/stage1/extractSession.js +47 -0
  71. package/dist/consolidation/stage1/extractSession.js.map +1 -0
  72. package/dist/consolidation/stage1/store.d.ts +45 -0
  73. package/dist/consolidation/stage1/store.d.ts.map +1 -0
  74. package/dist/consolidation/stage1/store.js +297 -0
  75. package/dist/consolidation/stage1/store.js.map +1 -0
  76. package/dist/consolidation/types.d.ts +49 -0
  77. package/dist/consolidation/types.d.ts.map +1 -0
  78. package/dist/consolidation/types.js +2 -0
  79. package/dist/consolidation/types.js.map +1 -0
  80. package/dist/fallback/llmRerank.d.ts.map +1 -1
  81. package/dist/fallback/llmRerank.js +9 -1
  82. package/dist/fallback/llmRerank.js.map +1 -1
  83. package/dist/fallback/sessionIndex.d.ts.map +1 -1
  84. package/dist/fallback/sessionIndex.js +15 -63
  85. package/dist/fallback/sessionIndex.js.map +1 -1
  86. package/dist/index.d.ts +4 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +4 -3
  89. package/dist/index.js.map +1 -1
  90. package/dist/local/graphQuery.d.ts +10 -0
  91. package/dist/local/graphQuery.d.ts.map +1 -1
  92. package/dist/local/graphQuery.js +25 -0
  93. package/dist/local/graphQuery.js.map +1 -1
  94. package/dist/pi-extension.d.ts.map +1 -1
  95. package/dist/pi-extension.js +356 -22
  96. package/dist/pi-extension.js.map +1 -1
  97. package/dist/preflight/detectIntents.d.ts.map +1 -1
  98. package/dist/preflight/detectIntents.js +7 -0
  99. package/dist/preflight/detectIntents.js.map +1 -1
  100. package/dist/preflight/hook.d.ts +18 -0
  101. package/dist/preflight/hook.d.ts.map +1 -1
  102. package/dist/preflight/hook.js +52 -7
  103. package/dist/preflight/hook.js.map +1 -1
  104. package/dist/preflight/render.d.ts +16 -2
  105. package/dist/preflight/render.d.ts.map +1 -1
  106. package/dist/preflight/render.js +24 -4
  107. package/dist/preflight/render.js.map +1 -1
  108. package/dist/service.d.ts +16 -0
  109. package/dist/service.d.ts.map +1 -1
  110. package/dist/service.js +57 -0
  111. package/dist/service.js.map +1 -1
  112. package/dist/session/activeBranch.d.ts +17 -0
  113. package/dist/session/activeBranch.d.ts.map +1 -0
  114. package/dist/session/activeBranch.js +251 -0
  115. package/dist/session/activeBranch.js.map +1 -0
  116. package/dist/settings.d.ts +5 -2
  117. package/dist/settings.d.ts.map +1 -1
  118. package/dist/settings.js +18 -1
  119. package/dist/settings.js.map +1 -1
  120. package/dist/tools/memoryAppend.d.ts +14 -4
  121. package/dist/tools/memoryAppend.d.ts.map +1 -1
  122. package/dist/tools/memoryAppend.js +46 -7
  123. package/dist/tools/memoryAppend.js.map +1 -1
  124. package/dist/tools/memoryRecall.d.ts +2 -2
  125. package/dist/tools/memoryRecall.d.ts.map +1 -1
  126. package/dist/tools/memoryRecall.js +35 -9
  127. package/dist/tools/memoryRecall.js.map +1 -1
  128. package/dist/trainer/extractFacts.d.ts +1 -1
  129. package/dist/trainer/index.d.ts +1 -1
  130. package/dist/trainer/index.d.ts.map +1 -1
  131. package/dist/trainer/index.js +1 -1
  132. package/dist/trainer/index.js.map +1 -1
  133. package/dist/trainer/scheduler.d.ts +2 -0
  134. package/dist/trainer/scheduler.d.ts.map +1 -1
  135. package/dist/trainer/scheduler.js +1 -0
  136. package/dist/trainer/scheduler.js.map +1 -1
  137. package/dist/trainer/sessionLoader.d.ts +3 -0
  138. package/dist/trainer/sessionLoader.d.ts.map +1 -1
  139. package/dist/trainer/sessionLoader.js +37 -74
  140. package/dist/trainer/sessionLoader.js.map +1 -1
  141. package/package.json +17 -4
  142. package/src/cache/memoryCaches.ts +72 -0
  143. package/src/cli.ts +144 -0
  144. package/src/config.ts +89 -1
  145. package/src/consolidation/enqueue.ts +205 -0
  146. package/src/consolidation/index.ts +90 -0
  147. package/src/consolidation/lock.ts +41 -0
  148. package/src/consolidation/log.ts +43 -0
  149. package/src/consolidation/memoryIndex.ts +127 -0
  150. package/src/consolidation/phase2/runPhase2.ts +255 -0
  151. package/src/consolidation/scheduler/launchd.ts +62 -0
  152. package/src/consolidation/scheduler/runConsolidate.ts +115 -0
  153. package/src/consolidation/scheduler/setupSchedule.ts +106 -0
  154. package/src/consolidation/scheduler/systemd.ts +78 -0
  155. package/src/consolidation/scheduler/types.ts +27 -0
  156. package/src/consolidation/scope.ts +58 -0
  157. package/src/consolidation/stage1/deltaExtract.ts +29 -0
  158. package/src/consolidation/stage1/drainQueue.ts +143 -0
  159. package/src/consolidation/stage1/extractSession.ts +58 -0
  160. package/src/consolidation/stage1/store.ts +411 -0
  161. package/src/consolidation/types.ts +52 -0
  162. package/src/fallback/llmRerank.ts +8 -1
  163. package/src/fallback/sessionIndex.ts +14 -51
  164. package/src/index.ts +8 -0
  165. package/src/local/graphQuery.ts +24 -0
  166. package/src/pi-extension.ts +393 -28
  167. package/src/preflight/detectIntents.ts +6 -0
  168. package/src/preflight/hook.ts +84 -7
  169. package/src/preflight/render.ts +35 -3
  170. package/src/service.ts +57 -0
  171. package/src/session/activeBranch.ts +294 -0
  172. package/src/settings.ts +37 -2
  173. package/src/tools/memoryAppend.ts +60 -7
  174. package/src/tools/memoryRecall.ts +33 -9
  175. package/src/trainer/index.ts +1 -1
  176. package/src/trainer/scheduler.ts +3 -0
  177. package/src/trainer/sessionLoader.ts +38 -87
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import fsPromises from "node:fs/promises";
3
+
4
+ import {
5
+ lineMatchesScope,
6
+ type MemoryScope,
7
+ } from "./scope.js";
8
+
9
+ export interface MemoryIndexCapOptions {
10
+ maxLines?: number;
11
+ maxBytes?: number;
12
+ scopes?: readonly MemoryScope[];
13
+ }
14
+
15
+ export interface MemoryIndexStats {
16
+ path: string;
17
+ exists: boolean;
18
+ lines: number;
19
+ bytes: number;
20
+ cappedLines: number;
21
+ cappedBytes: number;
22
+ }
23
+
24
+ const DEFAULT_MAX_LINES = 200;
25
+ const DEFAULT_MAX_BYTES = 25_600;
26
+
27
+ function capLines(
28
+ text: string,
29
+ opts: Required<Pick<MemoryIndexCapOptions, "maxLines" | "maxBytes">> & {
30
+ scopes?: readonly MemoryScope[];
31
+ },
32
+ ): string {
33
+ const out: string[] = [];
34
+ let bytes = 0;
35
+ for (const line of text.split("\n")) {
36
+ if (out.length >= opts.maxLines) break;
37
+ if (!lineMatchesScope(line, opts.scopes)) continue;
38
+ const nextBytes = Buffer.byteLength(line + "\n", "utf8");
39
+ if (bytes + nextBytes > opts.maxBytes) break;
40
+ out.push(line);
41
+ bytes += nextBytes;
42
+ }
43
+ return out.join("\n").trim();
44
+ }
45
+
46
+ function normalizedOptions(opts: MemoryIndexCapOptions = {}) {
47
+ return {
48
+ maxLines: opts.maxLines ?? DEFAULT_MAX_LINES,
49
+ maxBytes: opts.maxBytes ?? DEFAULT_MAX_BYTES,
50
+ scopes: opts.scopes,
51
+ };
52
+ }
53
+
54
+ export function readMemoryIndexCap(
55
+ paths: string[],
56
+ opts: MemoryIndexCapOptions = {},
57
+ ): string {
58
+ const normalized = normalizedOptions(opts);
59
+ for (const p of paths) {
60
+ let text: string;
61
+ try {
62
+ text = fs.readFileSync(p, "utf8");
63
+ } catch {
64
+ continue;
65
+ }
66
+ const capped = capLines(text, normalized);
67
+ if (capped) return capped;
68
+ }
69
+ return "";
70
+ }
71
+
72
+ export async function memoryIndexSnippet(
73
+ paths: string[],
74
+ query: string,
75
+ opts: MemoryIndexCapOptions = {},
76
+ ): Promise<string> {
77
+ const q = query.trim().toLowerCase();
78
+ if (!q) return "";
79
+ const normalized = normalizedOptions(opts);
80
+
81
+ for (const p of paths) {
82
+ let text: string;
83
+ try {
84
+ text = await fsPromises.readFile(p, "utf8");
85
+ } catch {
86
+ continue;
87
+ }
88
+ const matches: string[] = [];
89
+ let bytes = 0;
90
+ for (const line of text.split("\n")) {
91
+ if (!lineMatchesScope(line, normalized.scopes)) continue;
92
+ if (!line.toLowerCase().includes(q)) continue;
93
+ const nextBytes = Buffer.byteLength(line + "\n", "utf8");
94
+ if (bytes + nextBytes > normalized.maxBytes) break;
95
+ matches.push(line);
96
+ bytes += nextBytes;
97
+ if (matches.length >= normalized.maxLines) break;
98
+ }
99
+ if (matches.length > 0) return matches.join("\n");
100
+ }
101
+ return "";
102
+ }
103
+
104
+ export function getMemoryIndexStats(
105
+ paths: string[],
106
+ opts: MemoryIndexCapOptions = {},
107
+ ): MemoryIndexStats[] {
108
+ const normalized = normalizedOptions(opts);
109
+ return paths.map((p) => {
110
+ let text = "";
111
+ let exists = true;
112
+ try {
113
+ text = fs.readFileSync(p, "utf8");
114
+ } catch {
115
+ exists = false;
116
+ }
117
+ const capped = exists ? capLines(text, normalized) : "";
118
+ return {
119
+ path: p,
120
+ exists,
121
+ lines: exists ? text.split("\n").length : 0,
122
+ bytes: exists ? Buffer.byteLength(text, "utf8") : 0,
123
+ cappedLines: capped ? capped.split("\n").length : 0,
124
+ cappedBytes: Buffer.byteLength(capped, "utf8"),
125
+ };
126
+ });
127
+ }
@@ -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("&", "&amp;")
22
+ .replaceAll("<", "&lt;")
23
+ .replaceAll(">", "&gt;");
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
+ }