@seangle/pi-remember 0.0.0-stage → 0.2.0
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/CHANGELOG.md +14 -0
- package/LICENSE +202 -0
- package/README.md +263 -2
- package/command.ts +36 -0
- package/index.ts +751 -0
- package/inject.ts +45 -0
- package/migration.ts +458 -0
- package/notes.ts +131 -0
- package/package.json +49 -4
- package/search.ts +250 -0
- package/store.ts +730 -0
- package/summary.ts +70 -0
- package/workspace.ts +158 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
First public release as `@seangle/pi-remember`, licensed under Apache-2.0.
|
|
6
|
+
|
|
7
|
+
- Durable workspace-local and global facts, standalone Markdown notes, and lexical search.
|
|
8
|
+
- Stable session context with on-demand note retrieval.
|
|
9
|
+
- Explicit local initialization, memo updates, recoverable deletion and undo.
|
|
10
|
+
- Versioned storage with backed-up migration from the legacy note-reference format.
|
|
11
|
+
- Restore-path validation that refuses redirected recovery files and note directories.
|
|
12
|
+
- Pi package metadata and a source-only npm artifact; no build step.
|
|
13
|
+
|
|
14
|
+
Existing legacy stores migrate automatically when used. Back up stores, review the README's migration and concurrency limits, and upgrade every writer before using the new format.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,264 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-remember
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Durable memory for [pi](https://pi.dev/): a curated, plain-file knowledge cache that survives sessions. Save knowledge that was **expensive to obtain** and will matter again—decisions, preferences, lessons, environment quirks, research findings—not todos, debugging state, transcripts, or “the user asked me to X.” Pi's session files (`/resume`) already preserve session continuity.
|
|
4
|
+
|
|
5
|
+
| Scope | Store directory | Use for |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `local` (default for writes) | `<root>/.pi/memory/` | Knowledge about that workspace, repo, software, server, or configuration |
|
|
8
|
+
| `global` | `~/.pi/agent/memory/` | Facts about the user or machine itself: identity, OS, global tool preferences |
|
|
9
|
+
|
|
10
|
+
`root` defaults to the session working directory. `PI_REMEMBER_DIR` overrides the global directory. Specific software/configuration knowledge belongs in its workspace even when the software is installed machine-wide.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pi install npm:@seangle/pi-remember
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Restart Pi (or use `/reload` in an existing session) after installation. Use `/memory` for help and ask Pi to remember a durable fact to get started. Local writes require explicit initialization with the `memory_init` tool; global memory needs no initialization.
|
|
19
|
+
|
|
20
|
+
Requires Node.js **22.15 or newer**. Tested with Pi **1.4.0** (`@earendil-works/pi-coding-agent`); older Pi versions or other forks may not provide the host APIs this extension uses. Pi loads the TypeScript source directly; no build step or external service is needed.
|
|
21
|
+
|
|
22
|
+
**Upgrading from a manually installed copy:** remove or move the old extension directory out of `~/.pi/agent/extensions/` before loading the npm package, so the same tools are not registered twice. Keep your memory stores; they live separately from the extension. Review the migration section below before upgrading existing stores.
|
|
23
|
+
|
|
24
|
+
Source and issues: [Esensats/pi-remember](https://github.com/Esensats/pi-remember). The npm package is **`@seangle/pi-remember`**, not the unrelated unscoped `pi-remember` package.
|
|
25
|
+
|
|
26
|
+
## Architecture and file format
|
|
27
|
+
|
|
28
|
+
There is **no database, daemon, embedding service, or durable duplicate index**. Facts live in `MEMORY.md`; detailed notes are standalone Markdown files whose frontmatter is authoritative. A note catalogue is derived from those files when needed, never saved back into `MEMORY.md`.
|
|
29
|
+
|
|
30
|
+
Each store can contain:
|
|
31
|
+
|
|
32
|
+
| Path | Contents |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `MEMORY.md` | One-line facts and their metadata comments; not note references |
|
|
35
|
+
| `notes/<YYYY-MM-DD>-<slug>.md` | Standalone notes with YAML frontmatter and a Markdown body |
|
|
36
|
+
| `pi-remember.json` | Store format marker: `{"version": 1}` |
|
|
37
|
+
| `recovery/<recovery ID>.json` | Recoverable deletions and updates, including note snapshots |
|
|
38
|
+
| `migrations/v0-to-v1-<uuid>/` | Original-file backups and a manifest from each migration attempt |
|
|
39
|
+
|
|
40
|
+
Directories for notes and recovery are created only as needed. An absent store is not created merely by startup or migration checks.
|
|
41
|
+
|
|
42
|
+
### Facts
|
|
43
|
+
|
|
44
|
+
A fact is one line, at most 300 characters, with an eight-hex memo ID and a creation timestamp in the preceding HTML comment:
|
|
45
|
+
|
|
46
|
+
```markdown
|
|
47
|
+
<!-- 2026-10-18T16:10:33Z [a1b2c3d4] -->
|
|
48
|
+
#lesson Use the workspace's own virtual environment.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Facts currently have no last-modified date.** Updating a fact retains its ID and creation comment. Tags (`#lesson`) and wiki-links (`[[topic]]`) are optional text conventions, not required schema. Existing facts without IDs remain readable, but cannot be targeted by the ID-based update tool.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
description: "Python environment conventions"
|
|
58
|
+
memory-id: "e5f6a7b8"
|
|
59
|
+
created-date: "2026-10-18T16:10:33.000Z"
|
|
60
|
+
last-modified-date: "2026-10-18T16:10:33.000Z"
|
|
61
|
+
---
|
|
62
|
+
# Optional body heading
|
|
63
|
+
|
|
64
|
+
Confirmed details, rationale, and examples go here.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The four frontmatter fields are **`description`, `memory-id`, `created-date`, and `last-modified-date`**. The description is the short catalogue text; it is not a body heading. IDs identify memos independently of their paths.
|
|
68
|
+
|
|
69
|
+
`memory_store` retains the parameter name **`title` for backwards compatibility**, but it now means **description**. New descriptions must be nonempty, single-line, and at most 300 characters. Pass Markdown body text as `content`, **without frontmatter**. No H1 is generated; include your own heading in the body if useful.
|
|
70
|
+
|
|
71
|
+
Choose an optional `slug` of 1–80 letters/digits separated by hyphens, **without a date, path, or extension**. The filename always receives the UTC creation date and `.md`: `notes/2026-10-18-python-environments.md`. Omitting `slug` uses a description-derived slug for older callers. Filename collisions receive numbered suffixes rather than overwriting files.
|
|
72
|
+
|
|
73
|
+
The parser deliberately supports a **small scalar YAML subset**, not arbitrary YAML:
|
|
74
|
+
|
|
75
|
+
- Exactly the four known fields, each once; blank lines and comments are allowed.
|
|
76
|
+
- Plain scalars, single-quoted scalars (doubled apostrophes), or double-quoted scalars with JSON-compatible escapes. Quote ambiguous values.
|
|
77
|
+
- No nested structures, lists, multiline values, tags, anchors, aliases, unknown fields, or duplicate fields.
|
|
78
|
+
- A nonempty single-line description, an eight-hex ID, and valid ISO timestamps with time zones are required.
|
|
79
|
+
- The header, including delimiters, must fit within **16 KiB of UTF-8**. Metadata discovery reads in 512-byte chunks and stops at the closing delimiter, with a hard cap of 16 KiB plus one lookahead byte, rather than loading large note bodies.
|
|
80
|
+
|
|
81
|
+
A file beginning with a frontmatter delimiter but containing an invalid, incomplete, or oversized header is an error, not a legacy-note fallback. Invalid notes can make catalogue operations fail; optional session injection omits the affected scope instead of breaking the session. Ordinary text without frontmatter is treated as legacy input during migration.
|
|
82
|
+
|
|
83
|
+
## Session context and retrieval
|
|
84
|
+
|
|
85
|
+
At session start, pi-remember builds a snapshot containing facts plus a **frontmatter-only derived note catalogue**: description, memo ID, creation date, last-modified date, and path. Catalogue references may look like `#note [[notes/...]] Description`, but these are generated views, not persistent `MEMORY.md` entries. **Note bodies are not injected**; retrieve them explicitly by ID/path or search them on demand.
|
|
86
|
+
|
|
87
|
+
The snapshot is byte-stable between turns: `before_agent_start` appends the saved block without rereading files or making model calls. Writes/deletions and successful updates/restores to global or current-workspace memory rebuild it; another workspace's local changes do not replace the session's injected local memory. External file edits are not watched—start a fresh session to reliably pick them up.
|
|
88
|
+
|
|
89
|
+
Each scope has a default **3,000-character snapshot budget**. Oversized content keeps roughly the first 20% and last 80%, replacing the middle with a truncation notice. These are character budgets, not token/byte limits or strict final-output ceilings: the notice, scope headings, and enclosing instructions add overhead. The snapshot can omit entries or cut through an entry; read/search the store before making decisions based on it.
|
|
90
|
+
|
|
91
|
+
### Search
|
|
92
|
+
|
|
93
|
+
`memory_search` builds a **query-local BM25F-like lexical ranking** over facts and notes. No embeddings, vector database, persistent search index, or model call is involved.
|
|
94
|
+
|
|
95
|
+
- Unicode-normalized, case-insensitive **exact tokens**, not substring matches; common English stopwords and tokens shorter than two characters are omitted.
|
|
96
|
+
- Field weighting favors descriptions and tags/wiki-links; term-frequency saturation and length normalization limit repetition bias.
|
|
97
|
+
- Query coverage and contiguous phrase matches boost ranking. Quotes request a phrase boost, **not a mandatory phrase filter**.
|
|
98
|
+
- A note's description and body form one document, producing **one hit and one matching-passage snippet per note**, with scope/path/ID and an additional matching-line count where applicable.
|
|
99
|
+
- Managed frontmatter and HTML comments are excluded from searchable body text. Dates and IDs are metadata, not body keywords; use exact-ID reads for identity lookup.
|
|
100
|
+
- Up to 20 results are returned. Search reads bodies on demand; injection/catalogue discovery does not.
|
|
101
|
+
|
|
102
|
+
For a simple substring filter over fact/catalogue lines and metadata, use `memory_read({query})` instead.
|
|
103
|
+
|
|
104
|
+
## Tools
|
|
105
|
+
|
|
106
|
+
Every tool accepts optional `root`; it selects only local memory. Scope defaults differ intentionally:
|
|
107
|
+
|
|
108
|
+
| Tool | Parameters / behavior | Default scope |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `memory_init` | `{root?}`: explicitly initialize local memory in an existing workspace; preserve existing contents | local only |
|
|
111
|
+
| `memory_write` | `{content, scope?, root?}`: append one fact (≤300 chars) | `local` |
|
|
112
|
+
| `memory_store` | `{title, content, slug?, scope?, root?}`: create a standalone frontmatter note; `title` means description | `local` |
|
|
113
|
+
| `memory_read` | `{id?, query?, path?, scope?, root?}`: facts + derived catalogue, substring filter, exact memo lookup, or file read | `both` |
|
|
114
|
+
| `memory_search` | `{query, scope?, root?}`: ranked fact/note search with snippets | `both` |
|
|
115
|
+
| `memory_update` | `{id, content, scope?, root?}`: revise one existing memo in place; return recovery ID on change | `local` |
|
|
116
|
+
| `memory_forget` | `{text, scope?, root?}`: case-insensitive substring deletion by fact text or note description/path/identity metadata, **not note body** | `local` |
|
|
117
|
+
| `memory_restore` | `{recoveryId, scope?, root?}`: undo a deletion or update | `both` |
|
|
118
|
+
| `memory_status` | `{root?}`: store paths, counts, fact-file sizes, recoveries, and current session snapshot size | both stores |
|
|
119
|
+
|
|
120
|
+
New writes use normalized-text duplicate checks against facts and the derived catalogue. Duplicate note storage returns existing references **without creating or overwriting a note**; read and update the existing memo instead. Similar entries in the other scope can produce non-blocking warnings. These are text heuristics, not semantic deduplication or a body-comparison service.
|
|
121
|
+
|
|
122
|
+
Read/search outputs are bounded: catalogue/search output uses 20,000-byte / 400-line limits; note/ID reads use 30,000-byte / 600-line limits. Truncation keeps the tail and is reported. For a larger file, use pi's ordinary file-reading tools with offsets.
|
|
123
|
+
|
|
124
|
+
## Workspaces, initialization, and security
|
|
125
|
+
|
|
126
|
+
`root` is the **workspace directory containing `.pi`**, not `.pi` or the memory directory. Absolute paths, `~/` paths, and paths relative to the session cwd are accepted. The workspace must already exist; tools never create it. Symlinked workspace roots are canonicalized, and local access is rechecked after mutation-queue waits to avoid following a replaced root.
|
|
127
|
+
|
|
128
|
+
Roots are **per call**: accessing another workspace does not change subsequent default calls or the session's injected local memory. Global-only calls ignore `root` entirely and do not depend on local initialization. `scope: 'both'` combines global memory with the selected workspace's local store; it does not search every workspace.
|
|
129
|
+
|
|
130
|
+
Before using a local store, call `memory_init({root})` **only when you intend to create memory there**:
|
|
131
|
+
|
|
132
|
+
- It exclusively creates an empty `.pi/memory/MEMORY.md` if missing, never truncates existing memory, and never recreates a vanished workspace. Notes/recovery directories stay lazy; format setup may create version/migration files.
|
|
133
|
+
- An existing regular `MEMORY.md` already satisfies initialization; legacy data still needs the automatic format migration described below.
|
|
134
|
+
- Local tools, including reads/searches and default `both` calls, fail with an initialization instruction if `MEMORY.md` is missing. A notes-only local store still requires that file.
|
|
135
|
+
- `memory_status({root})` is the diagnostic exception: it works before initialization and never creates files or migrates the selected store. Its snapshot counts always describe the **current session**, not the selected root. Note counts include recursively discovered valid frontmatter notes. A note-only global store is reported even without a facts file.
|
|
136
|
+
- Global memory needs no explicit initialization. Use `scope: 'global'` from an uninitialized workspace rather than creating unwanted local memory just to make a search succeed.
|
|
137
|
+
|
|
138
|
+
Example tool calls from a session in another directory:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
memory_status({root: '~/homelab'})
|
|
142
|
+
memory_init({root: '~/homelab'}) // only if you intend to create its local store
|
|
143
|
+
memory_search({root: '~/homelab', scope: 'local', query: 'TV server'})
|
|
144
|
+
memory_store({
|
|
145
|
+
root: '~/homelab',
|
|
146
|
+
title: 'TV server configuration', // stored as description
|
|
147
|
+
slug: 'tv-server',
|
|
148
|
+
content: '# TV server\n\nConfirmed durable details…',
|
|
149
|
+
})
|
|
150
|
+
memory_read({root: '~/homelab', scope: 'local', id: 'e5f6a7b8'}) // use returned ID
|
|
151
|
+
memory_update({root: '~/homelab', id: 'e5f6a7b8', content: '# TV server\n\nFull revised body…'})
|
|
152
|
+
memory_restore({root: '~/homelab', scope: 'local', recoveryId: '<returned recovery ID>'})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Safe explicit path resolution rejects traversal/out-of-scope files and symlinks escaping a store. Catalogue discovery skips symlink aliases, and note/recovery writes refuse redirected subdirectories. Migration refuses symlinks within its input paths, including internal aliases. These checks are not a sandbox against hostile concurrent filesystem changes. Memory tools operate on local files with pi's permissions: `root` is not an SSH destination, and stored knowledge never grants remote-access permission. Do not store secrets or credentials.
|
|
156
|
+
|
|
157
|
+
## IDs, updates, deletion, and undo
|
|
158
|
+
|
|
159
|
+
A **memo ID** is eight hexadecimal characters, stored in a fact's metadata comment or a note's `memory-id`. A **recovery ID** identifies an undo record, not a memo. Use memo IDs with `memory_read`/`memory_update`, recovery IDs with `memory_restore`.
|
|
160
|
+
|
|
161
|
+
- `memory_read({id})` returns exactly one memo, including the complete stored note text subject to output limits. Do not combine `id` with `query` or `path`.
|
|
162
|
+
- IDs or relative paths present in both scopes are ambiguous with `scope: 'both'`; select `local` or `global`. Duplicate IDs within one scope require manual resolution, not a guessed match.
|
|
163
|
+
- `memory_update` requires a prior read. For facts, pass the replacement single line (≤300 chars); for notes, pass the **full revised body without frontmatter**, including any body heading you want to retain. Preserve unrelated/correct details.
|
|
164
|
+
- A note update preserves description, ID, creation date, and path; **`last-modified-date` advances only on an actual body change**. Fact updates do not add a modified date. Blank content is rejected; normalized/trim-equivalent no-ops leave bytes, timestamps, and recovery state untouched.
|
|
165
|
+
- Actual updates save recovery data before replacing the file. Targeted fact updates preserve unrelated bytes; file replacements are atomic and retain permissions. Extension mutations use pi's file-mutation queues. Note updates/undo use the note-file queue; deletion holds all selected note queues before snapshotting and removing them. New notes and recovery records are published atomically without overwriting existing paths.
|
|
166
|
+
- `memory_forget` saves matching entries and full note snapshots before removing them, including the note files themselves. For notes, the generated identity metadata used for matching contains the ID and creation date, **not the last-modified date**. Use a specific ID/path/description to avoid broad deletions.
|
|
167
|
+
- `memory_restore` must use the **same root** as the original change. It searches only the selected stores. Ambiguous recovery IDs, existing recovered paths/IDs, missing targets, and later edits cause refusal; conflicting recovery records are retained.
|
|
168
|
+
- Update undo checks the memo's identity and exact saved “after” content before restoring “before.” Note undo restores the **snapshot's metadata too**, including its previous last-modified date; it does not stamp the undo time. Successful undo consumes its recovery record. Deletion undo restores saved facts/notes without creating persistent note references and retains saved note permissions. Newly created notes/recovery records default to owner-only access (`0600`); new note/recovery directories use `0700`.
|
|
169
|
+
|
|
170
|
+
## Format versions and automatic migration
|
|
171
|
+
|
|
172
|
+
`pi-remember.json` inside each store records **format version 1**. An absent marker (or version `0`) means legacy format: note references were persisted in `MEMORY.md`, and generated notes could have a timestamp comment plus an automatic H1.
|
|
173
|
+
|
|
174
|
+
Migration runs automatically:
|
|
175
|
+
|
|
176
|
+
- At session startup for existing global and current-workspace stores.
|
|
177
|
+
- Lazily before ordinary reads/searches or mutations of another workspace; restore prepares its selected recovery store before changing it.
|
|
178
|
+
- Around initialization/first writes to establish the version of a newly created store. Missing stores are never created by migration itself.
|
|
179
|
+
|
|
180
|
+
**A read/search can therefore write migration data.** Status inspection does not migrate. Startup migration failures warn when UI is available and omit that scope from injection; explicit operations surface the error. Invalid or future versions are refused rather than guessed or overwritten, and version checks are repeated rather than permanently cached.
|
|
181
|
+
|
|
182
|
+
Migration validates its entire conversion plan before making backups or changing memory data. Each attempt creates **`migrations/v0-to-v1-<uuid>/`** containing a manifest (including modes, timestamps, and inference rules) and read-only, byte-for-byte backups of **all affected originals before any original is mutated**. Backups are retained even after success or a partial failure. The version marker is committed last; interrupted work can be retried with a new backup set. This is not a single multi-file transaction or an automatic rollback mechanism.
|
|
183
|
+
|
|
184
|
+
Conversion preserves existing memo IDs, note paths, fact bytes, unrelated index bytes, and file modes. It removes persisted note references and their managed comments from `MEMORY.md`, moving identity/description/dates into notes. Only the recognized generated legacy timestamp marker and its immediate generated H1/blank line are stripped; user-authored body headings remain. Existing valid frontmatter is retained. Unindexed legacy notes become discoverable standalone notes and receive IDs if needed.
|
|
185
|
+
|
|
186
|
+
Legacy notes had no recorded modified date: migration infers it from the original file's filesystem mtime. Creation comes from existing index/generated metadata where available, otherwise from mtime. These are documented inferences, not reconstructed edit history.
|
|
187
|
+
|
|
188
|
+
Recovery records migrate too, including old deletion records and whole-note update snapshots. **Forgotten notes are not resurrected**: legacy files left behind after deletion are snapshotted into recovery and removed from the active store. Recovery conversion happens before note/index edits so retries can retain identities and conflict checks. Malformed records, unsafe/dangling references, duplicate IDs, or conflicting metadata halt migration rather than silently dropping data.
|
|
189
|
+
|
|
190
|
+
### Concurrency and compatibility limits
|
|
191
|
+
|
|
192
|
+
In-process migration calls for the same canonical store share one run. An exclusively created **`.pi-remember-migration.lock`** serializes migrators across processes; existing locks cause a clear refusal, are never stolen, and require manual inspection if left after a crash. Backups are followed by a source-change check before applying the plan.
|
|
193
|
+
|
|
194
|
+
The migration lock is **not a general cross-process writer lock**. Pi's ordinary file-mutation queues coordinate cooperating operations within the host process, not other pi instances, editors, sync tools, or old binaries. Avoid concurrent writers during migration; a lock cannot make uncooperative writers safe.
|
|
195
|
+
|
|
196
|
+
**Old binaries are not safe to use against a migrated store. There is no backward-writer support or dual-format index.** Upgrade every writer; do not downgrade by deleting the version marker. New code refuses future versions, but old code may not understand the marker at all. Preserve the migration backups before any manual recovery or downgrade work.
|
|
197
|
+
|
|
198
|
+
## Commands
|
|
199
|
+
|
|
200
|
+
- **`/memory`** shows status in a notification, without starting an agent turn.
|
|
201
|
+
- **`/memory update`** sends an incremental memory-review reminder to the **current session's agent**. When idle it starts a normal turn; when busy it queues a **follow-up**, not an interruption. It uses the available conversation context (including compaction summaries) and normal tools, not a separate summarizer or bulk rewrite. Unknown arguments show usage; `update` is offered in completion.
|
|
202
|
+
|
|
203
|
+
The reminder asks the agent to read/search relevant memory, add missed durable knowledge, correct confirmed errors with `memory_update`, and recoverably remove obsolete entries. It preserves unrelated/correct knowledge, treats age alone as insufficient evidence, avoids secrets/task state, and permits no changes when nothing useful needs saving. The agent briefly reports its changes or that none were needed. **The command does not authorize remote actions or carry prior remote permission forward.** Reload extension changes with `/reload`.
|
|
204
|
+
|
|
205
|
+
## Configuration and opt-in exit summary
|
|
206
|
+
|
|
207
|
+
| Environment variable | Default | Purpose |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `PI_REMEMBER_DIR` | `~/.pi/agent/memory` | Global store directory |
|
|
210
|
+
| `PI_REMEMBER_INJECT_CHARS_GLOBAL` | `3000` | Global snapshot character budget, plus truncation/wrapper overhead |
|
|
211
|
+
| `PI_REMEMBER_INJECT_CHARS_LOCAL` | `3000` | Local snapshot character budget, plus truncation/wrapper overhead |
|
|
212
|
+
| `PI_REMEMBER_EXIT_SUMMARY` | off | Enable with `1`, `true`, `yes`, or `on` (case-insensitive) |
|
|
213
|
+
| `PI_REMEMBER_EXIT_SUMMARY_MODEL` | session model | Optional `provider/model-id` (or model ID); unavailable selection falls back to the session model |
|
|
214
|
+
| `PI_REMEMBER_EXIT_SUMMARY_SCOPE` | `auto` | `global`, `local`, or `auto`: local if the session workdir has `.pi`, otherwise global |
|
|
215
|
+
| `PI_REMEMBER_EXIT_SUMMARY_TIMEOUT_MS` | `10000` | Timeout for waiting on the summary model response |
|
|
216
|
+
|
|
217
|
+
Character budgets and timeout must parse as positive integers; invalid/nonpositive values use defaults. Zero does not disable injection. Configuration is loaded when the extension loads; reload/restart after changing it.
|
|
218
|
+
|
|
219
|
+
Exit summarization runs only on session shutdown with reason **`quit`**, when opted in. It sends up to the last 20,000 characters of user/assistant text from the current session branch to the selected model, asks for at most six concise durable fact lines (or `NONE`), filters the response, and appends accepted facts with normal duplicate checks. It is a model call that may incur cost and send conversation text to that provider—not purely local processing. No cheap model is automatically selected; choose one explicitly if desired.
|
|
220
|
+
|
|
221
|
+
A selected but uninitialized local store is **skipped**, not created and not redirected to global. Too little conversation text, missing model/authentication, errors, and timeouts are best-effort skips. The timeout stops waiting for the response; it does **not** cancel the underlying provider request. This opt-in extractor is separate from `/memory update` and does not perform its targeted review/correction workflow.
|
|
222
|
+
|
|
223
|
+
## Git and development
|
|
224
|
+
|
|
225
|
+
To keep workspace memory private, add this to the project's `.gitignore`:
|
|
226
|
+
|
|
227
|
+
```gitignore
|
|
228
|
+
.pi/memory/
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Committing memory shares it with the team. Review not just facts/notes but **recovery snapshots and migration backups**, which may retain removed information. Files are plain Markdown/JSON and can be reviewed in git; concurrent merges still require care with unique IDs and metadata.
|
|
232
|
+
|
|
233
|
+
No build step: pi loads TypeScript via jiti. There are no declared npm dependencies; host imports (`pi`, TypeBox, and TUI) are supplied by pi. For a one-off load, use `pi --extension ./index.ts` from this directory.
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
git clone git@github.com:Esensats/pi-remember.git
|
|
237
|
+
cd pi-remember
|
|
238
|
+
npm test
|
|
239
|
+
# node --experimental-strip-types --test "test/*.test.ts"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Use **Node >=22.15**: tests execute TypeScript directly with Node's experimental type stripping, and integration stubs use `module.registerHooks`. No separate TypeScript build or test runner is required. The tests use temporary fixture stores rather than live memory.
|
|
243
|
+
|
|
244
|
+
Source responsibilities:
|
|
245
|
+
|
|
246
|
+
| Module | Responsibility |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `store.ts` | Facts, standalone note storage/discovery, derived catalogue, IDs, updates, deletion/recovery, safe path lookup |
|
|
249
|
+
| `notes.ts` | Bounded frontmatter parsing/serialization and legacy generated-header handling |
|
|
250
|
+
| `migration.ts` | Version guards, validation/planning, backups, migration lock, conversion of notes and recovery records |
|
|
251
|
+
| `workspace.ts` | Existing-root resolution, explicit initialization, local-access guards |
|
|
252
|
+
| `search.ts` | Query-local lexical ranking and passage selection |
|
|
253
|
+
| `inject.ts` | Stable snapshot construction and middle truncation |
|
|
254
|
+
| `summary.ts` | Exit-summary text extraction, prompt, and response filtering |
|
|
255
|
+
| `command.ts` | `/memory` routing and incremental-review reminder (type-only pi import) |
|
|
256
|
+
| `index.ts` | Host integration: lifecycle, tools, queues, configuration, snapshot refresh, exit-summary model call |
|
|
257
|
+
|
|
258
|
+
Core modules have no pi runtime imports and are tested with plain Node; command behavior uses a stub API, and extension wiring tests stub host imports with Node module hooks.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
Copyright 2026 Esensats.
|
|
263
|
+
|
|
264
|
+
Licensed under the [Apache License, Version 2.0](LICENSE). Contributions are accepted under the same license.
|
package/command.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const MEMORY_UPDATE_PROMPT = `Please review durable memory in light of this conversation and update it only where needed. This is an incremental memory reminder, not a request to overhaul or rewrite the memory collection.
|
|
4
|
+
|
|
5
|
+
- Use the available conversation context (including any compaction summary) to identify durable knowledge that will help in future sessions: established decisions, preferences, hard-won lessons, and confirmed project/server setup or operational details. Save important knowledge you have not already saved.
|
|
6
|
+
- Read the current memory with memory_read and use memory_search for relevant topics; read related notes as needed before changing anything. The injected snapshot may be incomplete. Avoid duplicates and preserve unrelated or still-correct information. Do not scan or rewrite every note just for completeness.
|
|
7
|
+
- Correct or remove information only when this conversation provides evidence that it is outdated or wrong; age alone or not mentioning it is not evidence. Make the smallest targeted changes. Use memory_forget with a specific identifying string for obsolete facts or notes (deletions are recoverable), memory_write for one-line facts, and memory_store for new detailed notes. For memos with IDs, read by ID and use memory_update to edit the relevant file in place instead of forgetting/recreating facts or creating redundant replacement notes; preserve identity, description and unrelated content. Notes carry their own frontmatter; supply only the revised Markdown body, including any body heading worth keeping. Store migration is automatic; do not manually recreate an index.
|
|
8
|
+
- Default to local memory in the current workdir, including server-specific knowledge when working in a context directory such as ~/ctx/server-ABC. When the user requests memory in another workspace, use its root on each tool call, including reads and recovery; root is the workspace directory, not its .pi directory. Use memory_status to inspect a store before initialization, and memory_init only when you intend to create local memory in that existing directory; do not initialize a workspace merely to make a search succeed. Global memory is only for user/machine-wide facts, not particular servers, software, repos, or configs. Keep MEMORY.md for one-line facts only; put detailed knowledge in standalone notes with concise descriptions and chosen filename slugs. The date is prepended automatically; descriptions are injected from frontmatter, not duplicated in MEMORY.md.
|
|
9
|
+
- Do not store secrets, credentials, temporary task state, todos, or a transcript/session recap. Work from already available evidence; this command does not authorize remote access or imply that past remote-access permission carries into future sessions.
|
|
10
|
+
- If nothing useful needs changing, leave memory untouched. Briefly report what you added, corrected, or removed, or say that no updates were needed.`;
|
|
11
|
+
|
|
12
|
+
export function registerMemoryCommand(
|
|
13
|
+
pi: Pick<ExtensionAPI, "registerCommand" | "sendUserMessage">,
|
|
14
|
+
buildStatusText: () => Promise<string>,
|
|
15
|
+
): void {
|
|
16
|
+
pi.registerCommand("memory", {
|
|
17
|
+
description: "Show memory status, or /memory update to review and incrementally update memory",
|
|
18
|
+
getArgumentCompletions: (prefix) =>
|
|
19
|
+
"update".startsWith(prefix)
|
|
20
|
+
? [{ value: "update", label: "update", description: "Review this conversation and update memory only if needed" }]
|
|
21
|
+
: null,
|
|
22
|
+
handler: async (args, ctx) => {
|
|
23
|
+
const subcommand = args.trim();
|
|
24
|
+
if (subcommand === "update") {
|
|
25
|
+
// Start a normal turn when idle; otherwise wait for the ongoing work.
|
|
26
|
+
pi.sendUserMessage(MEMORY_UPDATE_PROMPT, { deliverAs: "followUp" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (subcommand) {
|
|
30
|
+
ctx.ui.notify("Usage: /memory (status) or /memory update", "warning");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
ctx.ui.notify(await buildStatusText(), "info");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|