@henryqw/pi-memory 1.0.1 → 1.1.1
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 +9 -27
- package/extensions/memory.ts +50 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,30 +18,26 @@ 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
22
|
| `memory` | tool | Add, replace, remove, or batch-edit entries across sessions. |
|
|
22
23
|
|
|
23
24
|
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.
|
|
24
25
|
|
|
25
|
-
At session start, the current contents of both stores are frozen into the system prompt; later edits during the session do not alter what the model already saw.
|
|
26
|
+
At session start, the current contents of both stores are frozen into the system prompt; later edits during the session do not alter what the model already saw. 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; unsuitable project-specific, temporary, trivial, or otherwise unsuitable content is refused. Each turn also includes a short memory check: save explicit durable preferences or corrections immediately, inferred habits 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.
|
|
26
27
|
|
|
27
28
|
To inspect live state, read `<directory>/MEMORY.md`.
|
|
28
29
|
|
|
29
30
|
## Config
|
|
30
31
|
|
|
31
|
-
`~/.pi/agent/config/pi-memory/config.json
|
|
32
|
+
Optional JSON file at the exact package-owned path `~/.pi/agent/config/pi-memory/config.json`. All fields are optional; a missing file uses defaults.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
- `directory`: absolute path where `MEMORY.md` and `USER.md` live. Required only when overriding the default (`~/.pi/agent/config/pi-memory/memory`). Point it at an iCloud- or Obsidian-synced folder to sync across machines.
|
|
42
|
-
- `memoryCharLimit` / `userCharLimit`: positive integers, maximum 100000.
|
|
34
|
+
| Field | Required | Possible values | Default |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `directory` | No | Non-empty absolute path without control characters, e.g. an iCloud- or Obsidian-synced folder | `~/.pi/agent/config/pi-memory/memory` |
|
|
37
|
+
| `memoryCharLimit` | No | Safe integer 1–100000 | `8800` |
|
|
38
|
+
| `userCharLimit` | No | Safe integer 1–100000 | `5500` |
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
Any other invalid configuration fails fast: malformed JSON, invalid UTF-8, files over 64 KiB, non-object roots, unknown keys, or out-of-range values throw an error naming the problem; the file is never rewritten.
|
|
45
41
|
|
|
46
42
|
## Storage & sync
|
|
47
43
|
|
|
@@ -52,17 +48,3 @@ Backups and the lock file live outside `directory`, under `~/.pi/agent/config/pi
|
|
|
52
48
|
## Threat model
|
|
53
49
|
|
|
54
50
|
Because the directory can be a globally synced location readable outside Pi, review [`ADR 006 — pi-memory global store threat model`](https://github.com/HenryQW/pi-packages/blob/main/docs/adr/006-pi-memory-global-store-threat-model.md) before pointing it at a shared or cloud-synced path.
|
|
55
|
-
|
|
56
|
-
## Remove
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
pi remove npm:@henryqw/pi-memory
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Development
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
npm test --workspace @henryqw/pi-memory
|
|
66
|
-
npm run typecheck --workspace @henryqw/pi-memory
|
|
67
|
-
npm run pack:check --workspace @henryqw/pi-memory
|
|
68
|
-
```
|
package/extensions/memory.ts
CHANGED
|
@@ -19,6 +19,8 @@ const DISPLAY_CONTROL_CHARACTER = /[\p{Cc}\p{Cf}]/gu;
|
|
|
19
19
|
// @henryqw/pi-herdr-btw does not export internal/core.ts from its package root.
|
|
20
20
|
const BTW_CHILD_PAYLOAD_ARG = "--pi-herdr-btw-payload";
|
|
21
21
|
const CONSOLIDATION_FAILURE = /(?:exceed|over) the limit|would put memory|no entry matched|[Mm]ultiple entries matched|matched multiple distinct/i;
|
|
22
|
+
const MEMORY_CHECK = "MEMORY CHECK: Save explicit durable user preferences or corrections immediately. 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.";
|
|
23
|
+
const REMEMBER_USAGE = "Usage: /remember <instruction>";
|
|
22
24
|
const MEMORY_DESCRIPTION = `Save durable facts to persistent memory that survive across sessions. Memory is injected into every future turn, so keep entries compact and high-signal.
|
|
23
25
|
|
|
24
26
|
HOW: Prefer one operations batch for multiple changes or consolidation. A batch applies atomically and checks the character limit only on the final result, so it can remove or shorten stale entries and add new ones in one call. Use action/content/old_text only for one lone change. A successful response finishes the update; do not repeat it.
|
|
@@ -101,6 +103,53 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
101
103
|
initError?: string;
|
|
102
104
|
} = { conflictWarnings: [] };
|
|
103
105
|
|
|
106
|
+
pi.registerCommand("remember", {
|
|
107
|
+
description: "Process an instruction into durable memory",
|
|
108
|
+
handler: async (args, ctx) => {
|
|
109
|
+
const candidate = args.trim();
|
|
110
|
+
if (!candidate) {
|
|
111
|
+
ctx.ui.notify(REMEMBER_USAGE, "warning");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!ctx.isIdle()) {
|
|
115
|
+
ctx.ui.notify("Cannot run /remember while the agent is busy.", "warning");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (state.initError) {
|
|
119
|
+
ctx.ui.notify(`Cannot run /remember: persistent memory is disabled — ${sanitizeName(state.initError)}`, "warning");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!state.config || !state.stores) {
|
|
123
|
+
ctx.ui.notify("Cannot run /remember: persistent memory is not initialized.", "warning");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const loaded = await Promise.all((Object.keys(state.stores) as Target[]).map(async (target) => [target, await state.stores![target].load(target)] as const));
|
|
128
|
+
const invalid = loaded.filter(([, result]) => result.status);
|
|
129
|
+
if (invalid.length) {
|
|
130
|
+
ctx.ui.notify(`Cannot run /remember: live memory state is unreadable or oversized. ${invalid.map(([, result]) => result.conflictWarning).join(" ")}`, "warning");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!ctx.isIdle()) {
|
|
134
|
+
ctx.ui.notify("Cannot run /remember while the agent is busy.", "warning");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const overLimit = loaded.filter(([target, result]) => {
|
|
138
|
+
const limit = target === "user" ? state.config!.userCharLimit : state.config!.memoryCharLimit;
|
|
139
|
+
return result.entries.join(ENTRY_DELIMITER).length > limit;
|
|
140
|
+
});
|
|
141
|
+
if (overLimit.length) {
|
|
142
|
+
ctx.ui.notify(`Cannot run /remember: live ${overLimit.map(([target]) => target).join(" and ")} entries exceed the configured character limit. Consolidate them before using /remember.`, "warning");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const entries = Object.fromEntries(loaded.map(([target, result]) => [target, result.entries]));
|
|
146
|
+
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)}`);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
ctx.ui.notify(`Cannot run /remember: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
104
153
|
pi.on("session_start", async () => {
|
|
105
154
|
state.config = undefined;
|
|
106
155
|
state.stores = undefined;
|
|
@@ -254,6 +303,6 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
254
303
|
}
|
|
255
304
|
if (!state.config || !state.stores || !state.snapshotBlocks) return;
|
|
256
305
|
const blocks = [...state.snapshotBlocks, ...state.conflictWarnings].filter(Boolean).join("\n\n");
|
|
257
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${blocks}` };
|
|
306
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${blocks ? `${blocks}\n\n` : ""}${MEMORY_CHECK}` };
|
|
258
307
|
});
|
|
259
308
|
}
|