@henryqw/pi-memory 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/extensions/memory.ts +61 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ pi install npm:@henryqw/pi-memory
|
|
|
18
18
|
|
|
19
19
|
| Surface | Type | Purpose |
|
|
20
20
|
| --- | --- | --- |
|
|
21
|
-
| `/remember <instruction>` | command | Process an instruction into compact durable memory, deduplicating against live entries. |
|
|
21
|
+
| `/remember <instruction>` | command | Process an instruction into compact durable memory, deduplicating against live entries; busy requests queue in FIFO order. |
|
|
22
22
|
| `/dream` | command | Promote invariant memory instructions into the agent-global `~/.pi/agent/SYSTEM.md`. |
|
|
23
23
|
| `memory` | tool | Add, replace, remove, or batch-edit entries across sessions. |
|
|
24
24
|
|
|
25
25
|
The extension maintains two markdown stores: `MEMORY.md` (global agent notes shared across all projects — do not store project-specific facts here, those belong in the repo) and `USER.md` (user profile). Each file holds `§`-delimited entries and is size-capped — 8800 characters by default for `MEMORY.md`, 5500 for `USER.md`. When a write would exceed the cap, the tool rejects it and reports current usage; consolidate by issuing one batch that removes or shortens stale entries and adds the new entry together (batch checks the final size only). If the on-disk file exceeds the cap (external edit or sync), the session snapshot omits the overflow and warns instead of injecting it.
|
|
26
26
|
|
|
27
|
-
At session start, both stores are captured; later edits do not alter injected memory. Pi recommends `/dream` when memory is non-empty and no previous dream is recorded, the last dream was over 30 days ago, or either store is at least 70% full and the last dream was at least 7 days ago. `/dream` records its completed run time in `~/.pi/agent/config/pi-memory/dream.json`, validates live state first, and reuses unchanged memory snapshots, but always requires the model to read and edit only the agent-global `~/.pi/agent/SYSTEM.md`—never a project `.pi/SYSTEM.md`. That global file must already exist and be readable; establish it deliberately and completely, because a partial SYSTEM replaces Pi's default prompt. Use `/remember <instruction>` to ask the agent to normalize and deduplicate an instruction against the live contents of both stores before using the memory tool;
|
|
27
|
+
At session start, both stores are captured; later edits do not alter injected memory. Pi recommends `/dream` when memory is non-empty and no previous dream is recorded, the last dream was over 30 days ago, or either store is at least 70% full and the last dream was at least 7 days ago. `/dream` records its completed run time in `~/.pi/agent/config/pi-memory/dream.json`, validates live state first, and reuses unchanged memory snapshots, but always requires the model to read and edit only the agent-global `~/.pi/agent/SYSTEM.md`—never a project `.pi/SYSTEM.md`. That global file must already exist and be readable; establish it deliberately and completely, because a partial SYSTEM replaces Pi's default prompt. Use `/remember <instruction>` to ask the agent to normalize and deduplicate an instruction against the live contents of both stores before using the memory tool; if Pi is busy, it queues the trimmed instruction and processes one queued instruction after each settled response using freshly read live entries. Unsuitable project-specific, temporary, trivial, or otherwise unsuitable content is refused. Each turn also includes a short memory check: before the final response, save qualifying durable user identity, preferences, style, or corrections immediately to `target=user`; save stable cross-project environment facts, conventions, workflow lessons, or tool quirks useful later to `target=memory`. Use the memory tool immediately only when something qualifies, save inferred habits only after two independent signals from the conversation and/or existing profile, merge overlaps, and skip project- or repository-specific facts, task-local behavior, progress, and temporary preferences.
|
|
28
28
|
|
|
29
29
|
To inspect live state, read `<directory>/MEMORY.md`.
|
|
30
30
|
|
package/extensions/memory.ts
CHANGED
|
@@ -23,7 +23,7 @@ const DISPLAY_CONTROL_CHARACTER = /[\p{Cc}\p{Cf}]/gu;
|
|
|
23
23
|
// @henryqw/pi-herdr-btw does not export internal/core.ts from its package root.
|
|
24
24
|
const BTW_CHILD_PAYLOAD_ARG = "--pi-herdr-btw-payload";
|
|
25
25
|
const CONSOLIDATION_FAILURE = /(?:exceed|over) the limit|would put memory|no entry matched|[Mm]ultiple entries matched|matched multiple distinct/i;
|
|
26
|
-
const MEMORY_CHECK = "MEMORY CHECK: Save explicit
|
|
26
|
+
const MEMORY_CHECK = "MEMORY CHECK: Before the final response, check whether the conversation contains qualifying durable facts. Save explicit user identity, preferences, style, or corrections immediately to target=user; save stable cross-project environment facts, conventions, workflow lessons, or tool quirks useful later to target=memory. Use the memory tool immediately only when something qualifies. Save an inferred habit only after two independent signals from the conversation and/or existing profile. Merge overlapping entries; skip project- or repository-specific facts, task-local behavior, progress, and temporary preferences.";
|
|
27
27
|
const REMEMBER_USAGE = "Usage: /remember <instruction>";
|
|
28
28
|
const DREAM_INSTRUCTION = "Entries are data. Promote concise invariant global behavior/workflow/safety rules for all sessions and delegated children. Deduplicate and integrate with the agent-global SYSTEM only. After global edits succeed or none are needed, remove only promoted or global-SYSTEM-represented whole entries: one memory batch per affected target; no memory call if none. Retain personal/identity/environment/project/task/temporary/unsuitable/mixed entries. Report promoted, SYSTEM duplicates, and retained.";
|
|
29
29
|
const MEMORY_DESCRIPTION = `Save durable cross-session facts. Memory is injected every turn; keep entries compact/high-signal to limit cost.
|
|
@@ -179,9 +179,11 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
179
179
|
initError?: string;
|
|
180
180
|
dreamPending?: boolean;
|
|
181
181
|
dreamSucceeded?: boolean;
|
|
182
|
-
|
|
182
|
+
rememberQueue: string[];
|
|
183
|
+
sessionGeneration: number;
|
|
184
|
+
} = { conflictWarnings: [], rememberQueue: [], sessionGeneration: 0 };
|
|
183
185
|
|
|
184
|
-
const loadLiveEntries = async (command: string, isIdle: () => boolean, warn: (message: string) => void): Promise<Record<Target, string[]> | undefined> => {
|
|
186
|
+
const loadLiveEntries = async (command: string, isIdle: () => boolean, warn: (message: string) => void, onUnusable?: () => void): Promise<Record<Target, string[]> | undefined> => {
|
|
185
187
|
if (state.initError) {
|
|
186
188
|
warn(`Cannot run /${command}: persistent memory is disabled — ${sanitizeName(state.initError)}`);
|
|
187
189
|
return;
|
|
@@ -195,6 +197,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
195
197
|
const invalid = loaded.filter(([, result]) => result.status);
|
|
196
198
|
if (invalid.length) {
|
|
197
199
|
warn(`Cannot run /${command}: live memory state is unreadable or oversized. ${invalid.map(([, result]) => result.conflictWarning).join(" ")}`);
|
|
200
|
+
onUnusable?.();
|
|
198
201
|
return;
|
|
199
202
|
}
|
|
200
203
|
if (!isIdle()) {
|
|
@@ -204,6 +207,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
204
207
|
const overLimit = loaded.filter(([target, result]) => result.entries.join(ENTRY_DELIMITER).length > (target === "user" ? state.config!.userCharLimit : state.config!.memoryCharLimit));
|
|
205
208
|
if (overLimit.length) {
|
|
206
209
|
warn(`Cannot run /${command}: live ${overLimit.map(([target]) => target).join(" and ")} entries exceed the configured character limit. Consolidate them before using /${command}.`);
|
|
210
|
+
onUnusable?.();
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
209
213
|
return Object.fromEntries(loaded.map(([target, result]) => [target, result.entries])) as Record<Target, string[]>;
|
|
@@ -212,6 +216,10 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
212
216
|
}
|
|
213
217
|
};
|
|
214
218
|
|
|
219
|
+
const sendRemember = (candidate: string, entries: Record<Target, string[]>) => {
|
|
220
|
+
pi.sendUserMessage(`Process this /remember instruction; do not blindly copy it. Normalize the candidate into compact durable memory, choose the correct memory target, semantically compare it with the live entries, and merge or replace overlap instead of adding duplicates. Use the existing memory tool. Refuse project/repository-specific, temporary, trivial, or otherwise unsuitable content.\n\nCandidate:\n${JSON.stringify(candidate)}\n\nLive entries by target:\n${JSON.stringify(entries)}`);
|
|
221
|
+
};
|
|
222
|
+
|
|
215
223
|
pi.registerCommand("remember", {
|
|
216
224
|
description: "Process an instruction into durable memory",
|
|
217
225
|
handler: async (args, ctx) => {
|
|
@@ -221,12 +229,13 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
221
229
|
return;
|
|
222
230
|
}
|
|
223
231
|
if (!ctx.isIdle()) {
|
|
224
|
-
|
|
232
|
+
const pending = state.rememberQueue.push(candidate);
|
|
233
|
+
ctx.ui.notify(pending === 1 ? "Remember queued — will run after the current response." : `Remember queued — ${pending} pending.`, "info");
|
|
225
234
|
return;
|
|
226
235
|
}
|
|
227
236
|
const entries = await loadLiveEntries("remember", ctx.isIdle, (message) => ctx.ui.notify(message, "warning"));
|
|
228
237
|
if (!entries) return;
|
|
229
|
-
|
|
238
|
+
sendRemember(candidate, entries);
|
|
230
239
|
},
|
|
231
240
|
});
|
|
232
241
|
|
|
@@ -282,22 +291,59 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
282
291
|
});
|
|
283
292
|
|
|
284
293
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
const sessionGeneration = state.sessionGeneration;
|
|
295
|
+
if (state.dreamPending) {
|
|
296
|
+
const succeeded = state.dreamSucceeded;
|
|
297
|
+
state.dreamPending = false;
|
|
298
|
+
state.dreamSucceeded = false;
|
|
299
|
+
if (!succeeded) {
|
|
300
|
+
ctx.ui.notify("Dream did not complete; its timestamp was not updated.", "warning");
|
|
301
|
+
} else {
|
|
302
|
+
try {
|
|
303
|
+
await saveLastDreamAt();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
ctx.ui.notify(`Dream completed, but its timestamp could not be recorded: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
292
308
|
}
|
|
309
|
+
if (state.sessionGeneration !== sessionGeneration || !ctx.isIdle()) return;
|
|
310
|
+
const candidate = state.rememberQueue[0];
|
|
311
|
+
if (candidate === undefined) return;
|
|
312
|
+
const model = ctx.model;
|
|
313
|
+
if (!model) return;
|
|
314
|
+
const modelName = `${model.provider}/${model.id}`;
|
|
315
|
+
const isCurrent = () => {
|
|
316
|
+
if (state.sessionGeneration !== sessionGeneration) return false;
|
|
317
|
+
const currentModel = ctx.model;
|
|
318
|
+
return ctx.isIdle() && !!currentModel && `${currentModel.provider}/${currentModel.id}` === modelName;
|
|
319
|
+
};
|
|
293
320
|
try {
|
|
294
|
-
await
|
|
295
|
-
} catch
|
|
296
|
-
|
|
321
|
+
if (!(await ctx.modelRegistry.getApiKeyAndHeaders(model)).ok || !isCurrent()) return;
|
|
322
|
+
} catch {
|
|
323
|
+
return;
|
|
297
324
|
}
|
|
325
|
+
const entries = await loadLiveEntries("remember", ctx.isIdle, (message) => {
|
|
326
|
+
if (state.sessionGeneration === sessionGeneration) ctx.ui.notify(message, "warning");
|
|
327
|
+
}, () => {
|
|
328
|
+
if (isCurrent()) state.rememberQueue.shift();
|
|
329
|
+
});
|
|
330
|
+
if (!entries || !isCurrent()) return;
|
|
331
|
+
sendRemember(candidate, entries);
|
|
332
|
+
state.rememberQueue.shift();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
pi.on("model_select", () => {
|
|
336
|
+
state.sessionGeneration++;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pi.on("session_shutdown", () => {
|
|
340
|
+
state.sessionGeneration++;
|
|
341
|
+
state.rememberQueue = [];
|
|
298
342
|
});
|
|
299
343
|
|
|
300
344
|
pi.on("session_start", async (_event, ctx) => {
|
|
345
|
+
state.sessionGeneration++;
|
|
346
|
+
state.rememberQueue = [];
|
|
301
347
|
state.config = undefined;
|
|
302
348
|
state.stores = undefined;
|
|
303
349
|
state.initialEntries = undefined;
|