@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.
Files changed (170) 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 +14 -4
  142. package/src/cli.ts +144 -0
  143. package/src/config.ts +89 -1
  144. package/src/consolidation/enqueue.ts +205 -0
  145. package/src/consolidation/index.ts +90 -0
  146. package/src/consolidation/lock.ts +41 -0
  147. package/src/consolidation/log.ts +43 -0
  148. package/src/consolidation/memoryIndex.ts +127 -0
  149. package/src/consolidation/phase2/runPhase2.ts +255 -0
  150. package/src/consolidation/scheduler/launchd.ts +62 -0
  151. package/src/consolidation/scheduler/runConsolidate.ts +115 -0
  152. package/src/consolidation/scheduler/setupSchedule.ts +106 -0
  153. package/src/consolidation/scheduler/systemd.ts +78 -0
  154. package/src/consolidation/scheduler/types.ts +27 -0
  155. package/src/consolidation/scope.ts +58 -0
  156. package/src/consolidation/stage1/deltaExtract.ts +29 -0
  157. package/src/consolidation/stage1/drainQueue.ts +143 -0
  158. package/src/consolidation/stage1/extractSession.ts +58 -0
  159. package/src/consolidation/stage1/store.ts +411 -0
  160. package/src/consolidation/types.ts +52 -0
  161. package/src/fallback/sessionIndex.ts +14 -51
  162. package/src/index.ts +6 -0
  163. package/src/pi-extension.ts +309 -10
  164. package/src/preflight/hook.ts +16 -2
  165. package/src/preflight/render.ts +7 -0
  166. package/src/session/activeBranch.ts +294 -0
  167. package/src/settings.ts +37 -2
  168. package/src/tools/memoryAppend.ts +60 -7
  169. package/src/trainer/index.ts +1 -1
  170. package/src/trainer/sessionLoader.ts +38 -87
@@ -0,0 +1,294 @@
1
+ import path from "node:path";
2
+
3
+ interface UnknownRecord {
4
+ [key: string]: unknown;
5
+ }
6
+
7
+ export interface ActiveBranchTurn {
8
+ role: string;
9
+ content: string;
10
+ turnIndex: number;
11
+ }
12
+
13
+ export interface ParsedSession {
14
+ id: string;
15
+ title: string;
16
+ createdAt: string;
17
+ turns: ActiveBranchTurn[];
18
+ parentSessionId?: string;
19
+ parentSessionFile?: string;
20
+ }
21
+
22
+ interface JsonlNodeInfo {
23
+ parentId?: string;
24
+ parentSessionId?: string;
25
+ parentSessionFile?: string;
26
+ }
27
+
28
+ interface ParsedJsonlLine {
29
+ id?: string;
30
+ parentId?: string;
31
+ parentSessionId?: string;
32
+ parentSessionFile?: string;
33
+ message?: {
34
+ role?: string;
35
+ content?: unknown;
36
+ };
37
+ role?: string;
38
+ content?: unknown;
39
+ timestamp?: string;
40
+ createdAt?: string;
41
+ }
42
+
43
+ function asString(value: unknown): string | undefined {
44
+ if (typeof value === "string") {
45
+ const text = value.trim();
46
+ return text.length > 0 ? text : undefined;
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ function getStringField(obj: UnknownRecord, names: string[]): string | undefined {
52
+ for (const name of names) {
53
+ const found = asString(obj[name]);
54
+ if (found) return found;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function getBooleanField(obj: UnknownRecord, names: string[]): boolean | undefined {
60
+ for (const name of names) {
61
+ if (typeof obj[name] === "boolean") return obj[name];
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ function messageText(content: unknown): string {
67
+ if (typeof content === "string") return content;
68
+ if (!Array.isArray(content)) return "";
69
+
70
+ const parts: string[] = [];
71
+ for (const block of content) {
72
+ if (typeof block === "string") {
73
+ parts.push(block);
74
+ continue;
75
+ }
76
+ if (block && typeof block === "object") {
77
+ const b = block as UnknownRecord;
78
+ if (typeof b.text === "string") parts.push(b.text);
79
+ else if (typeof b.content === "string") parts.push(b.content);
80
+ }
81
+ }
82
+ return parts.join("\n");
83
+ }
84
+
85
+ function extractMessage(line: ParsedJsonlLine): { role: string; content: unknown } | null {
86
+ if (line.message?.role && line.message.content !== undefined) {
87
+ return { role: line.message.role, content: line.message.content };
88
+ }
89
+ if (line.role && line.content !== undefined) {
90
+ return { role: line.role, content: line.content };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function pickActiveLeafHint(obj: UnknownRecord): string | undefined {
96
+ const explicitByName = getStringField(obj, [
97
+ "activeSessionId",
98
+ "active_session_id",
99
+ "activeSession",
100
+ "active_session",
101
+ "activeLeafId",
102
+ "active_leaf_id",
103
+ "activeLeafSessionId",
104
+ "active_leaf_session_id",
105
+ "currentSessionId",
106
+ "current_session_id",
107
+ "leafSessionId",
108
+ "leaf_session_id",
109
+ ]);
110
+ if (explicitByName) return explicitByName;
111
+
112
+ if (getBooleanField(obj, ["active", "is_active", "isActive", "activeLeaf"])
113
+ && typeof obj.id === "string") {
114
+ return obj.id;
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ function parseSessionIdFromPath(filePath: string | undefined): string | undefined {
120
+ const normalized = asString(filePath);
121
+ if (!normalized) return undefined;
122
+ const base = path.basename(normalized, path.extname(normalized));
123
+ const underscore = base.lastIndexOf("_");
124
+ if (underscore >= 0 && underscore < base.length - 1) {
125
+ return base.slice(underscore + 1);
126
+ }
127
+ return base || undefined;
128
+ }
129
+
130
+ export function parseJsonlSession(raw: string, filePath: string): ParsedSession | null {
131
+ const lines = raw.split("\n").filter((line) => line.trim());
132
+ if (lines.length === 0) return null;
133
+
134
+ const parsedLines: ParsedJsonlLine[] = [];
135
+ const nodes = new Map<string, JsonlNodeInfo>();
136
+
137
+ let headerSessionId: string | undefined;
138
+ let headerTitle = "";
139
+ let headerCreatedAt = "";
140
+ let lastNodeId: string | undefined;
141
+ let explicitLeafHint: string | undefined;
142
+ let fallbackParentSessionId: string | undefined;
143
+ let fallbackParentSessionFile: string | undefined;
144
+
145
+ for (const rawLine of lines) {
146
+ let obj: UnknownRecord;
147
+ try {
148
+ obj = JSON.parse(rawLine) as UnknownRecord;
149
+ } catch {
150
+ continue;
151
+ }
152
+
153
+ const id = getStringField(obj, ["id", "entryId", "entry_id"]);
154
+ const parentId = getStringField(obj, ["parentId", "parent_id"]);
155
+ const parentSessionId = getStringField(obj, [
156
+ "parentSessionId",
157
+ "parent_session_id",
158
+ ]);
159
+ const parentSessionFile = getStringField(obj, [
160
+ "parentSessionFile",
161
+ "parent_session_file",
162
+ "parentSessionPath",
163
+ "parent_session_path",
164
+ "parentSession",
165
+ "parent_session",
166
+ ]);
167
+ const type = asString(obj.type);
168
+ const message: ParsedJsonlLine["message"] = obj.message as ParsedJsonlLine["message"] | undefined;
169
+
170
+ const line: ParsedJsonlLine = {
171
+ id,
172
+ parentId,
173
+ parentSessionId,
174
+ parentSessionFile,
175
+ message,
176
+ role: asString(obj.role),
177
+ content: obj.content,
178
+ timestamp: asString(obj.timestamp),
179
+ createdAt: asString(obj.createdAt),
180
+ };
181
+
182
+ const explicitLeafId = pickActiveLeafHint(obj);
183
+ if (explicitLeafId) explicitLeafHint = explicitLeafId;
184
+
185
+ if (type === "session") {
186
+ headerSessionId = asString(obj.id) ?? headerSessionId;
187
+ headerTitle = asString(obj.title) ?? headerTitle;
188
+ headerCreatedAt = asString(obj.timestamp) ?? asString(obj.created_at) ?? asString(obj.createdAt) ?? headerCreatedAt;
189
+ if (!fallbackParentSessionId) {
190
+ fallbackParentSessionId = getStringField(obj, ["parentSessionId", "parent_session_id"]);
191
+ }
192
+ if (!fallbackParentSessionFile) {
193
+ fallbackParentSessionFile = getStringField(obj, [
194
+ "parentSessionFile",
195
+ "parent_session_file",
196
+ "parentSessionPath",
197
+ "parent_session_path",
198
+ "parentSession",
199
+ "parent_session",
200
+ ]);
201
+ }
202
+ if (!fallbackParentSessionId && fallbackParentSessionFile) {
203
+ fallbackParentSessionId = parseSessionIdFromPath(fallbackParentSessionFile);
204
+ }
205
+ }
206
+
207
+ if (id && type !== "session") {
208
+ nodes.set(id, {
209
+ parentId,
210
+ parentSessionId,
211
+ parentSessionFile,
212
+ });
213
+ lastNodeId = id;
214
+ }
215
+
216
+ parsedLines.push(line);
217
+ }
218
+
219
+ if (parsedLines.length === 0) return null;
220
+
221
+ const hasBranchNodes = nodes.size > 0;
222
+ let activeLeafId = explicitLeafHint ?? lastNodeId;
223
+ if (activeLeafId && explicitLeafHint && explicitLeafHint !== lastNodeId && !nodes.has(activeLeafId)) {
224
+ activeLeafId = lastNodeId;
225
+ }
226
+ const activeSessionId = activeLeafId ?? headerSessionId ?? path.basename(filePath, path.extname(filePath));
227
+ let parentSessionId: string | undefined = fallbackParentSessionId;
228
+ let parentSessionFile: string | undefined = fallbackParentSessionFile;
229
+
230
+ const activeNodeIds = new Set<string>();
231
+ if (hasBranchNodes && activeLeafId) {
232
+ let cursor: string | undefined = activeLeafId;
233
+ const leafNode = nodes.get(activeLeafId);
234
+ if (leafNode) {
235
+ if (leafNode.parentSessionId) {
236
+ parentSessionId = leafNode.parentSessionId;
237
+ }
238
+ if (leafNode.parentSessionFile) {
239
+ parentSessionFile = leafNode.parentSessionFile;
240
+ }
241
+ if (!parentSessionId && parentSessionFile) {
242
+ parentSessionId = parseSessionIdFromPath(parentSessionFile);
243
+ }
244
+ }
245
+ for (let i = 0; i < nodes.size + 5; i++) {
246
+ if (!cursor || activeNodeIds.has(cursor)) break;
247
+ activeNodeIds.add(cursor);
248
+ const node = nodes.get(cursor);
249
+ if (!node) break;
250
+ if (!node.parentId) break;
251
+ cursor = node.parentId;
252
+ if (!nodes.has(cursor)) break;
253
+ }
254
+ }
255
+
256
+ const includeLine = (line: ParsedJsonlLine): boolean => {
257
+ if (!hasBranchNodes || !activeLeafId) return true;
258
+ if (!line.id && !line.parentId) return false;
259
+ if (line.id) return activeNodeIds.has(line.id);
260
+ if (line.parentId) return activeNodeIds.has(line.parentId);
261
+ return false;
262
+ };
263
+
264
+ const turns: ActiveBranchTurn[] = [];
265
+ let turnIndex = 0;
266
+ for (const line of parsedLines) {
267
+ if (!includeLine(line)) continue;
268
+
269
+ const messageObj = extractMessage(line);
270
+ if (!messageObj?.role || !messageObj.content) continue;
271
+ if (messageObj.role !== "user" && messageObj.role !== "assistant") continue;
272
+ const text = messageText(messageObj.content);
273
+ if (!text.trim()) continue;
274
+
275
+ turns.push({
276
+ role: messageObj.role,
277
+ content: text,
278
+ turnIndex: turnIndex++,
279
+ });
280
+ }
281
+
282
+ if (turns.length === 0) return null;
283
+
284
+ return {
285
+ id: activeSessionId,
286
+ title: headerTitle,
287
+ createdAt: headerCreatedAt,
288
+ turns,
289
+ parentSessionId,
290
+ parentSessionFile,
291
+ };
292
+ }
293
+
294
+ export { messageText };
package/src/settings.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  defaultMemoryConfig,
8
8
  normalizeMemoryConfig,
9
9
  type ExtractorType,
10
+ type ConsolidationConfig,
10
11
  type MemoryConfig,
11
12
  type MemoryProvider,
12
13
  type TrainerConfig,
@@ -31,6 +32,7 @@ export interface MemorySettingsFile {
31
32
  /** OpenAI-compatible endpoint config (vLLM, SGLang, LM Studio, etc.) */
32
33
  vllm?: Partial<OpenAICompatConfig>;
33
34
  trainer?: Partial<TrainerConfig>;
35
+ consolidation?: Partial<ConsolidationConfig>;
34
36
  }
35
37
 
36
38
  export interface LoadedMemorySettings {
@@ -70,11 +72,19 @@ export function loadMemorySettings(
70
72
  configPath = defaultMemoryConfigPath(),
71
73
  ): LoadedMemorySettings {
72
74
  const fileSettings = readMemorySettingsFile(configPath);
73
- const { helperModel, ollama, vllm, trainer, ...configFields } = fileSettings;
75
+ const {
76
+ helperModel,
77
+ ollama,
78
+ vllm,
79
+ trainer,
80
+ consolidation,
81
+ ...configFields
82
+ } = fileSettings;
74
83
 
75
84
  const config = normalizeMemoryConfig({
76
85
  ...configFields,
77
86
  ...(trainer ? { trainer } : {}),
87
+ ...(consolidation ? { consolidation } : {}),
78
88
  ...overrides,
79
89
  } as Partial<MemoryConfig> & Record<string, unknown>);
80
90
 
@@ -122,5 +132,30 @@ export function resolveHelperModelSpec(
122
132
  return settingsHelperModel;
123
133
  }
124
134
 
135
+ /** Write memory settings back to disk (preserves unknown fields). */
136
+ export function saveMemorySettings(
137
+ updates: Partial<MemorySettingsFile>,
138
+ configPath = defaultMemoryConfigPath(),
139
+ ): void {
140
+ let existing: Record<string, unknown> = {};
141
+ try {
142
+ const raw = fs.readFileSync(configPath, "utf8");
143
+ const parsed = JSON.parse(raw);
144
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
145
+ existing = parsed as Record<string, unknown>;
146
+ }
147
+ } catch { /* start fresh */ }
148
+
149
+ const merged = { ...existing, ...updates };
150
+ const dir = path.dirname(configPath);
151
+ fs.mkdirSync(dir, { recursive: true });
152
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
153
+ }
154
+
125
155
  export { defaultMemoryConfig };
126
- export type { ExtractorType, MemoryProvider, TrainerConfig };
156
+ export type {
157
+ ConsolidationConfig,
158
+ ExtractorType,
159
+ MemoryProvider,
160
+ TrainerConfig,
161
+ };
@@ -2,18 +2,20 @@ import fs from "node:fs/promises";
2
2
  import { open } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
+ import { openConsolidationStore } from "../consolidation/stage1/store.js";
6
+ import type { MemoryScope } from "../consolidation/scope.js";
5
7
  import type { ToolResult } from "../types.js";
6
8
 
7
9
  export const MEMORY_APPEND_NAME = "memory_append";
8
10
 
9
11
  export const MEMORY_APPEND_DESCRIPTION =
10
- "Append new entries to MEMORY.md. Use this instead of file_write or file_edit for memory updates. " +
11
- "Writes are append-only with a simple lock to avoid concurrent clobber.";
12
+ "Persist durable memory only when the user explicitly asks to remember something. " +
13
+ "Writes go through the pi-memory consolidation stage instead of direct file edits when configured.";
12
14
 
13
- export const MEMORY_APPEND_PROMPT_SNIPPET = "Append durable notes to MEMORY.md";
15
+ export const MEMORY_APPEND_PROMPT_SNIPPET = "Persist explicit user memory";
14
16
 
15
17
  export const MEMORY_APPEND_PROMPT_GUIDELINES = [
16
- "Use memory_append to persist user preferences or facts the user explicitly asked to remember — not for transient task state.",
18
+ "Use memory_append only when the user explicitly asks to remember a preference or durable fact — not for transient task state.",
17
19
  ] as const;
18
20
 
19
21
  export const MEMORY_APPEND_PARAMETERS = {
@@ -71,8 +73,52 @@ export async function appendToMemoryMd(
71
73
  });
72
74
  }
73
75
 
76
+ export interface Stage1AppendOptions {
77
+ dbPath: string;
78
+ sessionId?: string;
79
+ sessionFile?: string;
80
+ scope?: MemoryScope;
81
+ now?: string;
82
+ }
83
+
84
+ export async function appendToStage1(
85
+ content: string,
86
+ opts: Stage1AppendOptions,
87
+ ): Promise<string> {
88
+ const trimmed = content.trim();
89
+ if (!trimmed) throw new Error("content must not be empty");
90
+ const store = openConsolidationStore(opts.dbPath);
91
+ if (!store) throw new Error("could not open consolidation store");
92
+ const now = opts.now ?? new Date().toISOString();
93
+ const sessionId = opts.sessionId
94
+ ? `manual_${opts.sessionId}_${Date.now()}`
95
+ : `manual_${Date.now()}`;
96
+ try {
97
+ store.upsertStage1Output({
98
+ session_id: sessionId,
99
+ session_file: opts.sessionFile ?? "manual-memory-append",
100
+ source_mtime_ms: Date.now(),
101
+ generated_at: now,
102
+ raw_memory: trimmed,
103
+ rollout_summary: trimmed,
104
+ scope: opts.scope ?? "global",
105
+ status: "done",
106
+ selected_for_phase2: false,
107
+ usage_count: 0,
108
+ last_usage: now,
109
+ error_message: null,
110
+ });
111
+ return sessionId;
112
+ } finally {
113
+ store.close();
114
+ }
115
+ }
116
+
74
117
  export class MemoryAppendTool {
75
- constructor(private readonly memoryMdPath: string) {}
118
+ constructor(
119
+ private readonly memoryMdPath: string,
120
+ private readonly stage1?: Stage1AppendOptions,
121
+ ) {}
76
122
 
77
123
  info() {
78
124
  return {
@@ -97,6 +143,10 @@ export class MemoryAppendTool {
97
143
  return { content: "content must not be empty", isError: true };
98
144
  }
99
145
  try {
146
+ if (this.stage1) {
147
+ const id = await appendToStage1(content, this.stage1);
148
+ return { content: `queued memory for consolidation: ${id}` };
149
+ }
100
150
  await appendToMemoryMd(this.memoryMdPath, content);
101
151
  return { content: `appended to ${this.memoryMdPath}` };
102
152
  } catch (e) {
@@ -108,6 +158,9 @@ export class MemoryAppendTool {
108
158
  }
109
159
  }
110
160
 
111
- export function createMemoryAppendTool(memoryMdPath: string): MemoryAppendTool {
112
- return new MemoryAppendTool(memoryMdPath);
161
+ export function createMemoryAppendTool(
162
+ memoryMdPath: string,
163
+ stage1?: Stage1AppendOptions,
164
+ ): MemoryAppendTool {
165
+ return new MemoryAppendTool(memoryMdPath, stage1);
113
166
  }
@@ -129,7 +129,7 @@ export async function trainBundle(
129
129
  };
130
130
  }
131
131
 
132
- export { loadSessions } from "./sessionLoader.js";
132
+ export { loadSessionFile, loadSessions } from "./sessionLoader.js";
133
133
  export type { LoadedSession, SessionTurn, SessionLoaderOptions } from "./sessionLoader.js";
134
134
  export { extractFacts, extractFactsFromSessions, RELATION_CATALOG, ALL_RELATIONS } from "./extractFacts.js";
135
135
  export type {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { messageText, parseJsonlSession as parseActiveBranchJsonl } from "../session/activeBranch.js";
4
5
 
5
6
  export interface SessionTurn {
6
7
  role: string;
@@ -14,6 +15,8 @@ export interface LoadedSession {
14
15
  createdAt: string;
15
16
  filePath: string;
16
17
  modifiedAt: Date;
18
+ parentSessionFile?: string;
19
+ parentSessionId?: string;
17
20
  turns: SessionTurn[];
18
21
  }
19
22
 
@@ -29,39 +32,6 @@ interface PiSessionFile {
29
32
  messages?: PiSessionMessage[];
30
33
  }
31
34
 
32
- interface JsonlSessionHeader {
33
- type: "session";
34
- id?: string;
35
- timestamp?: string;
36
- title?: string;
37
- }
38
-
39
- interface JsonlMessageLine {
40
- type: "message";
41
- message?: {
42
- role?: string;
43
- content?: unknown;
44
- };
45
- }
46
-
47
- function messageText(content: unknown): string {
48
- if (typeof content === "string") return content;
49
- if (!Array.isArray(content)) return "";
50
- const parts: string[] = [];
51
- for (const block of content) {
52
- if (typeof block === "string") {
53
- parts.push(block);
54
- continue;
55
- }
56
- if (block && typeof block === "object") {
57
- const b = block as Record<string, unknown>;
58
- if (typeof b.text === "string") parts.push(b.text);
59
- else if (typeof b.content === "string") parts.push(b.content);
60
- }
61
- }
62
- return parts.join("\n");
63
- }
64
-
65
35
  export interface SessionLoaderOptions {
66
36
  sessionsDir: string;
67
37
  modifiedAfter?: Date | null;
@@ -94,49 +64,6 @@ async function collectSessionFiles(dir: string): Promise<string[]> {
94
64
  return files;
95
65
  }
96
66
 
97
- function parseJsonlSession(raw: string, filePath: string): LoadedSession | null {
98
- const lines = raw.split("\n").filter((l) => l.trim());
99
- if (lines.length === 0) return null;
100
-
101
- let header: JsonlSessionHeader | null = null;
102
- const turns: SessionTurn[] = [];
103
- let turnIndex = 0;
104
-
105
- for (const line of lines) {
106
- let obj: Record<string, unknown>;
107
- try {
108
- obj = JSON.parse(line) as Record<string, unknown>;
109
- } catch {
110
- continue;
111
- }
112
-
113
- if (obj.type === "session" && !header) {
114
- header = obj as unknown as JsonlSessionHeader;
115
- continue;
116
- }
117
-
118
- if (obj.type === "message") {
119
- const msg = (obj as unknown as JsonlMessageLine).message;
120
- if (!msg?.role || !msg.content) continue;
121
- if (msg.role !== "user" && msg.role !== "assistant") continue;
122
- const text = messageText(msg.content);
123
- if (!text.trim()) continue;
124
- turns.push({ role: msg.role, content: text, turnIndex: turnIndex++ });
125
- }
126
- }
127
-
128
- if (turns.length === 0) return null;
129
-
130
- return {
131
- id: header?.id ?? path.basename(filePath, path.extname(filePath)),
132
- title: header?.title ?? "",
133
- createdAt: header?.timestamp ?? "",
134
- filePath,
135
- modifiedAt: new Date(),
136
- turns,
137
- };
138
- }
139
-
140
67
  function parseJsonSession(raw: string, filePath: string): LoadedSession | null {
141
68
  let session: PiSessionFile;
142
69
  try {
@@ -167,6 +94,16 @@ function parseJsonSession(raw: string, filePath: string): LoadedSession | null {
167
94
  };
168
95
  }
169
96
 
97
+ function parseJsonlSession(raw: string, filePath: string): LoadedSession | null {
98
+ const parsed = parseActiveBranchJsonl(raw, filePath);
99
+ if (!parsed) return null;
100
+ return {
101
+ ...parsed,
102
+ filePath,
103
+ modifiedAt: new Date(),
104
+ };
105
+ }
106
+
170
107
  /**
171
108
  * Scan session files (JSON + JSONL, recursive subdirectories), parse Pi session
172
109
  * format, optionally filter by modified-after timestamp for incremental training.
@@ -190,17 +127,7 @@ export async function loadSessions(
190
127
  if (!st.isFile()) continue;
191
128
  if (modifiedAfter && st.mtime <= modifiedAfter) continue;
192
129
 
193
- let raw: string;
194
- try {
195
- raw = await fs.readFile(filePath, "utf8");
196
- } catch {
197
- continue;
198
- }
199
-
200
- const isJsonl = filePath.endsWith(".jsonl");
201
- const parsed = isJsonl
202
- ? parseJsonlSession(raw, filePath)
203
- : parseJsonSession(raw, filePath);
130
+ const parsed = await loadSessionFile(filePath);
204
131
 
205
132
  if (!parsed) continue;
206
133
  parsed.modifiedAt = st.mtime;
@@ -211,6 +138,30 @@ export async function loadSessions(
211
138
  return deduplicateSessions(sessions);
212
139
  }
213
140
 
141
+ export async function loadSessionFile(filePath: string): Promise<LoadedSession | null> {
142
+ let st: Awaited<ReturnType<typeof fs.stat>>;
143
+ try {
144
+ st = await fs.stat(filePath);
145
+ } catch {
146
+ return null;
147
+ }
148
+ if (!st.isFile()) return null;
149
+
150
+ let raw: string;
151
+ try {
152
+ raw = await fs.readFile(filePath, "utf8");
153
+ } catch {
154
+ return null;
155
+ }
156
+
157
+ const parsed = filePath.endsWith(".jsonl")
158
+ ? parseJsonlSession(raw, filePath)
159
+ : parseJsonSession(raw, filePath);
160
+ if (!parsed) return null;
161
+ parsed.modifiedAt = st.mtime;
162
+ return parsed;
163
+ }
164
+
214
165
  function deduplicateSessions(sessions: LoadedSession[]): LoadedSession[] {
215
166
  const seen = new Set<string>();
216
167
  return sessions.filter((s) => {