@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,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 {
|
|
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 {
|
|
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
|
-
"
|
|
11
|
-
"Writes
|
|
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 = "
|
|
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
|
|
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(
|
|
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(
|
|
112
|
-
|
|
161
|
+
export function createMemoryAppendTool(
|
|
162
|
+
memoryMdPath: string,
|
|
163
|
+
stage1?: Stage1AppendOptions,
|
|
164
|
+
): MemoryAppendTool {
|
|
165
|
+
return new MemoryAppendTool(memoryMdPath, stage1);
|
|
113
166
|
}
|
package/src/trainer/index.ts
CHANGED
|
@@ -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
|
-
|
|
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) => {
|