pi-memory-evolution 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 +246 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/docs/conversation-recall.md +94 -0
- package/docs/core-quality.md +224 -0
- package/docs/design.md +328 -0
- package/docs/progress-pipeline.md +188 -0
- package/docs/quality-validation.md +85 -0
- package/docs/review-0.2.md +82 -0
- package/docs/testing.md +102 -0
- package/docs/usage.md +386 -0
- package/package.json +61 -0
- package/src/adapter/operations.ts +95 -0
- package/src/adapter/pi-api.ts +24 -0
- package/src/adapter/progress-observation.ts +83 -0
- package/src/adapter/session-context.ts +36 -0
- package/src/child-process.ts +8 -0
- package/src/index.ts +256 -0
- package/src/injector/digest.ts +29 -0
- package/src/memory/evolution.ts +64 -0
- package/src/memory/extractor.ts +63 -0
- package/src/memory/feedback.ts +11 -0
- package/src/memory/learning.ts +24 -0
- package/src/memory/legacy.ts +92 -0
- package/src/memory/memory-store.ts +502 -0
- package/src/memory/privacy.ts +51 -0
- package/src/memory/progress-targets.ts +52 -0
- package/src/memory/quality.ts +81 -0
- package/src/memory/query.ts +87 -0
- package/src/memory/recovery.ts +23 -0
- package/src/memory/retriever.ts +181 -0
- package/src/memory/search.ts +105 -0
- package/src/memory/sqlite.ts +7 -0
package/docs/usage.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Usage and operations reference
|
|
2
|
+
|
|
3
|
+
See the [README](../README.md) for a project overview and installation.
|
|
4
|
+
This reference covers detailed behavior and optional controls; normal use is automatic.
|
|
5
|
+
|
|
6
|
+
- [Installation and updates](#installation-and-updates)
|
|
7
|
+
- [Automatic learning and recall](#what-happens-automatically)
|
|
8
|
+
- [Evidence, decay and ranking](#evidence-decay-and-self-ranking)
|
|
9
|
+
- [Storage and privacy](#local-storage-and-provenance)
|
|
10
|
+
- [Commands](#commands)
|
|
11
|
+
- [Migration](#migration-from-01)
|
|
12
|
+
- [Recovery and troubleshooting](#recovery-and-troubleshooting)
|
|
13
|
+
|
|
14
|
+
## Installation and updates
|
|
15
|
+
|
|
16
|
+
Use Pi 0.85+ with a configured model. The runtime can be standalone Pi/Bun or npm
|
|
17
|
+
Pi on Node 22.19+. Git installation also requires Git and npm on PATH. Pi supplies
|
|
18
|
+
its own APIs and TypeBox; the extension uses built-in SQLite.
|
|
19
|
+
|
|
20
|
+
Choose **one** source:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# npm package
|
|
24
|
+
pi install npm:pi-memory-evolution
|
|
25
|
+
|
|
26
|
+
# Git default branch (main), without a historical tag pin
|
|
27
|
+
pi install https://github.com/btnalit/pi-memory-evolution
|
|
28
|
+
|
|
29
|
+
# Local checkout: direct reference, no copy
|
|
30
|
+
pi install /absolute/path/to/pi-memory-evolution
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run `/reload` and `/memory status` inside Pi. There is no separate installer,
|
|
34
|
+
post-install script, owner approval, or additional API key to configure. For local
|
|
35
|
+
development, install dev dependencies with `npm ci --ignore-scripts` in the checkout.
|
|
36
|
+
|
|
37
|
+
Before upgrading, read [migration and backups](#migration-from-01). Update the same
|
|
38
|
+
source you installed:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pi update npm:pi-memory-evolution
|
|
42
|
+
# Or, for a Git install:
|
|
43
|
+
pi update https://github.com/btnalit/pi-memory-evolution
|
|
44
|
+
# For a local checkout, update it yourself:
|
|
45
|
+
cd /absolute/path/to/pi-memory-evolution
|
|
46
|
+
git switch main
|
|
47
|
+
git pull --ff-only origin main
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Plain `pi update` updates Pi itself, not this package. `PI_OFFLINE=1` also suppresses
|
|
51
|
+
explicit package updates; unset it or use `PI_OFFLINE=0` when updating. Package
|
|
52
|
+
subcommands do not accept the general `--offline` flag.
|
|
53
|
+
|
|
54
|
+
Pinned npm versions stay pinned. The old `v0.1.0` Git tag is historical; install the
|
|
55
|
+
unpinned Git source to follow the default branch instead. Use `pi list` and remove
|
|
56
|
+
the old source before switching between npm/Git/local forms, which Pi treats as
|
|
57
|
+
different package identities. `pi remove <source>` does not erase memory state.
|
|
58
|
+
Restart or `/reload` all sessions using the extension after changing versions.
|
|
59
|
+
|
|
60
|
+
## What happens automatically
|
|
61
|
+
|
|
62
|
+
- A successful `session_compact` saves a sanitized source and extracts up to 16
|
|
63
|
+
facts, preferences, decisions or project-state claims using recognizable headings.
|
|
64
|
+
- Explicit user statements containing cues such as `remember`, `prefer`, `记住`,
|
|
65
|
+
`偏好`, `纠正`, `不对`, `以后`, or `不要` also trigger learning, without waiting for
|
|
66
|
+
another compaction. Natural declarations such as `我比较在意的三大功能…`,
|
|
67
|
+
`我们的核心需求是…` or `Our priorities are…` also trigger learning, even if followed
|
|
68
|
+
by a question asking for feedback. This is bounded intent recognition, not universal
|
|
69
|
+
understanding. Quotes, ordinary recall questions and one-off commands are not requirements.
|
|
70
|
+
Assistant/tool text never becomes a user memory instruction.
|
|
71
|
+
- Work requests (commit/push/fix/test/review, etc.) can update existing project states
|
|
72
|
+
from linked tool results. Important commit/push/test results are retained ahead of
|
|
73
|
+
late routine inspection, not simply the last 8 tools. Normal completion and interrupted
|
|
74
|
+
assistant responses are distinguished: a completed tool operation is usable evidence
|
|
75
|
+
even if the final reply failed, but it does **not** prove the entire task completed.
|
|
76
|
+
Internal `memory_recall` results and observations referencing this extension's own
|
|
77
|
+
state directory are excluded from this evidence path.
|
|
78
|
+
- Update nomination is separate from answering a query: explicit operation resources and
|
|
79
|
+
project names nominate up to 8 active, unpinned states in the same capture origin,
|
|
80
|
+
prioritizing pending states without a per-path top-2 or answer-deduplication gate.
|
|
81
|
+
Expired states can receive new evidence; forgotten/conflicted states cannot. The model
|
|
82
|
+
can only replace nominated project states, never create preferences from tool results.
|
|
83
|
+
A mixed requirement/work turn can create **two separate serialized sources/calls**,
|
|
84
|
+
preserving statement versus tool authority instead of silently discarding the work.
|
|
85
|
+
- Each processing attempt makes at most one background model call, using up to 32
|
|
86
|
+
recently updated active memories from that source's capture origin. This is a
|
|
87
|
+
conservative automatic-replacement safeguard, **not a recall restriction**.
|
|
88
|
+
It uses **the current Pi session model and Pi's own provider/auth resolution**.
|
|
89
|
+
With no model override in the session, this is Pi's configured default model.
|
|
90
|
+
There is no extra API key, provider setting, subagent, or alternate-model fallback.
|
|
91
|
+
- Valid additions/replacements commit immediately, with provenance and before/after
|
|
92
|
+
history. Inferred memories remain labeled `provisional`, but are recallable without
|
|
93
|
+
approval. Pinned memories cannot be automatically replaced.
|
|
94
|
+
- Replayed source events are idempotent. Calls have a **120-second deadline** and an
|
|
95
|
+
**8192-output-token cap** (clamped to the active model's smaller limit). They are
|
|
96
|
+
cancelled on session shutdown/reload. Structured summary claims survive model failure.
|
|
97
|
+
User-cue prose has no local-extraction fallback: its sanitized source is saved, but
|
|
98
|
+
learning its claims requires a successful model attempt.
|
|
99
|
+
- Session start checks persisted work across **all origins**. While Pi remains running,
|
|
100
|
+
a local recovery timer checks every **15 seconds** (after the previous check/call ends),
|
|
101
|
+
gradually draining eligible pending/failed work one source per check. Selection favors
|
|
102
|
+
the earliest retry time, then oldest capture; a failed source does not block other work.
|
|
103
|
+
- Failed attempts retry automatically after **1 minute, 5 minutes, 15 minutes, then 1 hour**.
|
|
104
|
+
After **5 consecutive failures**, that source pauses and a warning points to diagnostics;
|
|
105
|
+
there are no indefinite paid probes. Retry times/budgets survive reloads and restarts.
|
|
106
|
+
`/memory status` shows safe error categories, failure times, attempt counts, next retry
|
|
107
|
+
times and paused counts. `/memory evolve` remains an optional one-off override of the
|
|
108
|
+
delay/limit, not the normal recovery path; successful/retired jobs are never rerun.
|
|
109
|
+
- A job lease lasts **150 seconds** (120-second deadline plus 30-second grace). The timer
|
|
110
|
+
detects expired running jobs and schedules them with the same bounded backoff. Shutdown/
|
|
111
|
+
reload cancellation returns work to pending without consuming the failure budget.
|
|
112
|
+
Timers stop at shutdown and do not keep a print-mode process alive. Recovery resumes
|
|
113
|
+
next time Pi runs; this is not a standalone daemon.
|
|
114
|
+
- Recall searches **the whole memory database**, including previous sessions, other
|
|
115
|
+
directories and existing legacy claims. No project-directory startup or manual adoption
|
|
116
|
+
is needed. Matching uses exact literals, word segmentation and weighted topic coverage
|
|
117
|
+
locally, without a model call. Paths/filenames do not earn extra votes for their component
|
|
118
|
+
words; for example, a repository named `pi-memory-evolution` is not itself evidence
|
|
119
|
+
about cross-session memory.
|
|
120
|
+
- Natural questions such as `X相关记忆你还记得吗?` or `What do you remember about X?`
|
|
121
|
+
separate the recall request from its actual subject. This applies across topics, not
|
|
122
|
+
through per-device exceptions. Technical questions about memory/recall remain topics.
|
|
123
|
+
Asking what Pi remembers does not itself trigger a paid learning call.
|
|
124
|
+
- Vague follow-ups use bounded recent **user** messages from the active branch, including
|
|
125
|
+
retained compaction tails. Multi-hop refinements such as `SQLite 数据库 → 端口呢? →
|
|
126
|
+
认证呢? → 继续` retain the subject and require the current attribute; prior-topic-only
|
|
127
|
+
matches cannot satisfy the new question. Explicit new, unknown and reset topics stop
|
|
128
|
+
old-topic inheritance. Assistant/tool/injected text never supplies the topic.
|
|
129
|
+
The active-context scan covers up to 4096 entries/messages, returning at most six
|
|
130
|
+
user texts; repeated topic-less continuations share a slot so long tool-heavy work
|
|
131
|
+
does not immediately lose its subject. Bounds and compaction still limit recall.
|
|
132
|
+
A fresh session saying only `继续` injects nothing; naming a topic enables cross-session
|
|
133
|
+
recall regardless of its original directory.
|
|
134
|
+
- Query coverage, evidence-based document frequency, field weights, mild length
|
|
135
|
+
normalization and a relative cutoff reject weak secondary matches. Unseen query words
|
|
136
|
+
no longer receive the highest rarity weight. Exact paths must match, and a quoted
|
|
137
|
+
question in a replay/incident note is weaker than evidence answering it. Redundancy
|
|
138
|
+
filtering cannot let a project-state note hide a preference of the same origin.
|
|
139
|
+
Source IDs/cwd have no authority bonus. After relevance gates, host-assigned evidence,
|
|
140
|
+
type-specific freshness and explicit feedback order eligible matches; pin/date break remaining ties.
|
|
141
|
+
Common words such as `没有` or `现在` cannot trigger recall.
|
|
142
|
+
At most three claims fit within **2048 UTF-8 bytes**, with origin/source labels and
|
|
143
|
+
non-truncatable trust guidance. Fewer matches means fewer claims, not padding with recent
|
|
144
|
+
records. Identical content from different origins retains separate provenance.
|
|
145
|
+
Unpinned project-state claims still age out after seven days; pinning exempts age, not truth checks.
|
|
146
|
+
Short named-attribute queries require the subject and attribute, rather than substituting
|
|
147
|
+
another subject/attribute when the correct memory is missing or suppressed.
|
|
148
|
+
- Common Chinese/English concepts are normalized locally for existing records. Evolution
|
|
149
|
+
can add up to 8 validated bilingual `searchTerms` per claim, extending matching without
|
|
150
|
+
changing its factual text or adding a recall-time translation call. Aliases alone do
|
|
151
|
+
not refresh evidence dates. This is bounded bilingual support, not universal translation.
|
|
152
|
+
|
|
153
|
+
A model call may incur the usual charges of your active provider. These background
|
|
154
|
+
calls are not assistant turns and their usage is not added to Pi's session token totals.
|
|
155
|
+
There is no additional call on ordinary recall. Eligible work turns may now incur one
|
|
156
|
+
additional background call each; no related tracked state or no tool observation means
|
|
157
|
+
no progress call. A mixed statement/work turn may additionally incur a separate learning
|
|
158
|
+
call; interrupted turns with usable observations may also learn through automatic recovery.
|
|
159
|
+
Automatic retries may add up to four calls per source after the initial
|
|
160
|
+
failure; every attempt uses the session's then-current model/authentication. Model mistakes
|
|
161
|
+
remain possible; tool observations and model-generated aliases are not proof of truth. Use
|
|
162
|
+
history, correction, pinning and undo rather than treating generated claims as verified facts.
|
|
163
|
+
|
|
164
|
+
## Evidence, decay and self-ranking
|
|
165
|
+
|
|
166
|
+
- New claims carry **host-assigned** evidence: `summary`, `user_statement`,
|
|
167
|
+
`tool_observation` or `manual_correction`, plus extraction method, source ID and date.
|
|
168
|
+
Model output cannot supply confidence/verification/feedback fields. Old records without
|
|
169
|
+
this metadata remain `unknown`; migration does not guess their source or certify them.
|
|
170
|
+
- Source appropriateness matters: a user's stated preference is stronger evidence of that
|
|
171
|
+
preference than a summary; a linked tool observation supports a project state, not a
|
|
172
|
+
user preference or independent proof of success. A weaker model-proposed replacement
|
|
173
|
+
is withheld and its new variant quarantined, with history. Current user corrections
|
|
174
|
+
and new tool-backed project progress can still update automatically; pins remain protected.
|
|
175
|
+
- Freshness decreases smoothly by type: project state fastest, then facts, decisions,
|
|
176
|
+
preferences. Stable kinds retain a nonzero floor and do not expire. Project states keep
|
|
177
|
+
the seven-day safety cap; no upgrade revives old states. Read/search/injection, pinning,
|
|
178
|
+
alias enrichment, feedback and conflict resolution do not reset the evidence clock.
|
|
179
|
+
- Ranking keeps **relevance, evidence, freshness and feedback separate**. Quality cannot
|
|
180
|
+
rescue an unrelated/weak lexical match. `useful` is not `accurate`; neither is independent
|
|
181
|
+
verification. Repeated retrieval or repeated positive feedback earns no cumulative boost.
|
|
182
|
+
`/memory show` and `/memory explain` expose the factors. Injected claims include evidence
|
|
183
|
+
labels and an aging warning, not a fictitious probability of truth.
|
|
184
|
+
|
|
185
|
+
Optional `/memory feedback <id> useful|unhelpful|accurate|incorrect` records a precise user
|
|
186
|
+
verdict without a model call. `unhelpful` modestly lowers utility, not factual credibility.
|
|
187
|
+
`incorrect` quarantines the claim and same-origin exact duplicates, retires known pending
|
|
188
|
+
repeats, and is undoable. Use `correct` for new content or `resolve` to restore a disputed
|
|
189
|
+
claim; conflict/resolution does not rejuvenate old evidence. Feedback with a timestamp
|
|
190
|
+
older than the current content's evidence date is ignored. Receipts prevent replay after restart/undo; identical repeated verdicts do not
|
|
191
|
+
accumulate weight. Feedback receipts, like history, are not securely erased by forget.
|
|
192
|
+
Whole user messages `记忆 <24-hex-id> 有用。` / `memory <24-hex-id> incorrect` also work;
|
|
193
|
+
quotes, questions, assistant/tool text and vague “wrong” do not identify a feedback target.
|
|
194
|
+
Ordinary natural-language corrections continue through automatic model evolution.
|
|
195
|
+
|
|
196
|
+
The model also gets a **read-only `memory_recall` tool** for missing background discovered
|
|
197
|
+
mid-task. It takes an explicit topic and returns up to three relevant claims / 2048 UTF-8
|
|
198
|
+
bytes, using the same lifecycle and quality gates. It does not modify memory, persist a
|
|
199
|
+
query in the memory DB, or make an additional retrieval-model call (normal agent tool
|
|
200
|
+
turns still incur normal usage and appear in the Pi transcript). Automatic per-user-turn
|
|
201
|
+
injection remains the default; the model chooses whether a second lookup is needed.
|
|
202
|
+
Explicit tool allowlists must include `memory_recall`; the extension never overrides them.
|
|
203
|
+
|
|
204
|
+
These are bounded evidence policies, not learned semantic verification, independent-source
|
|
205
|
+
corroboration, or a universally accurate self-evolving ranker. See
|
|
206
|
+
[core quality design and validation](core-quality.md) for formulas and remaining gaps.
|
|
207
|
+
|
|
208
|
+
## Local storage and provenance
|
|
209
|
+
|
|
210
|
+
State lives under Pi's public agent directory (`getAgentDir()`). By default:
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
~/.pi/agent/agent-suite/memory-evolution/
|
|
214
|
+
├── memory.sqlite # memories, sources, suppression hashes, jobs and history
|
|
215
|
+
├── memory.sqlite-wal # SQLite-managed when open
|
|
216
|
+
└── memory.sqlite-shm
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
If `PI_CODING_AGENT_DIR` is set, replace `~/.pi/agent` with that directory. Using a
|
|
220
|
+
different agent directory means a different database. Changing cwd does **not** hide
|
|
221
|
+
memories in that database or create a recall boundary.
|
|
222
|
+
|
|
223
|
+
SQLite transactions/WAL protect concurrent processes and interrupted commits.
|
|
224
|
+
Model calls run outside transactions; stale responses cannot overwrite intervening
|
|
225
|
+
changes. Raw summaries are evidence only, **never a separate recall fallback**, so
|
|
226
|
+
forgetting a derived claim cannot expose it again through its parent summary.
|
|
227
|
+
|
|
228
|
+
The stored `scope` field records capture origin (canonical cwd, not an inferred project
|
|
229
|
+
identity). It is retained for provenance and conservative write protection, not eligibility
|
|
230
|
+
for recall. An origin can cover multiple projects; facts must retain explicit subject names
|
|
231
|
+
where available. Explicit origin identifiers can help a named-context query at low weight;
|
|
232
|
+
current cwd, `legacy` labels and source IDs do not boost a claim's authority or relevance.
|
|
233
|
+
|
|
234
|
+
Global recall does **not** mean global rewriting: automatic replacement candidates remain
|
|
235
|
+
within the source origin, and the model must identify the same subject/fact, not just a
|
|
236
|
+
matching port or path. Cross-origin variants are not automatically merged or overwritten.
|
|
237
|
+
Exact-ID manual corrections/forget work from any session and affect the selected record
|
|
238
|
+
and exact duplicates within its origin, **not identical text from unrelated origins**.
|
|
239
|
+
Suppression hashes likewise remain origin-qualified. Arbitrary paraphrases or semantic
|
|
240
|
+
identity across origins cannot be resolved reliably by a local hash. These safeguards
|
|
241
|
+
may leave ambiguous variants for inspection rather than guessing which one to retire.
|
|
242
|
+
Forget is logical suppression, not secure erasure of history or the original Pi transcript.
|
|
243
|
+
|
|
244
|
+
Sensitive lines/blocks are suppressed before capture, edits, model submission and
|
|
245
|
+
recall. This covers common token/password/JSON/Chinese/Bearer/private-key formats,
|
|
246
|
+
including quoted multiline values, indented YAML blocks and control-character cleanup,
|
|
247
|
+
but not every possible secret. Do not rely on a regex as a complete DLP system. Sanitized
|
|
248
|
+
sources and selected existing memories go to the already-configured Pi model provider.
|
|
249
|
+
|
|
250
|
+
## Commands
|
|
251
|
+
|
|
252
|
+
These are optional direct controls, **not approval gates**:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
/memory list [page] # all origins, 20 non-forgotten records per page
|
|
256
|
+
/memory list all [page] # alias for list
|
|
257
|
+
/memory list here [page] # optional current-origin view
|
|
258
|
+
/memory list legacy [page] # optional unknown-origin view
|
|
259
|
+
/memory show <id> # exact ID, any scope/status; includes provenance
|
|
260
|
+
/memory search <query> # up to 10 recallable matches across all origins
|
|
261
|
+
/memory explain # last automatic recall snapshot, including injection count
|
|
262
|
+
/memory explain <query> # preview retrieval reasons for an explicit query (3-claim cap)
|
|
263
|
+
/memory learning # last capture/nomination reasons + recent transaction outcomes
|
|
264
|
+
/memory status # integrity + retries + processed-versus-changed outcomes
|
|
265
|
+
/memory history # last 10 events across all origins
|
|
266
|
+
/memory evolve # optional one-off retry, overriding delay/failure limit
|
|
267
|
+
/memory undo <event-id> # reverse actual changes, if not modified since
|
|
268
|
+
/memory feedback <id> <verdict> # useful | unhelpful | accurate | incorrect
|
|
269
|
+
/memory correct <id> <replacement> # literal replacement, 4–480 characters
|
|
270
|
+
/memory forget <id>
|
|
271
|
+
/memory pin <id>
|
|
272
|
+
/memory unpin <id>
|
|
273
|
+
/memory conflict <id> <other-id> # suppress both
|
|
274
|
+
/memory resolve <id> # restore this conflicted side; other stays suppressed
|
|
275
|
+
/memory adopt <id> # optionally label a legacy claim with this directory
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Pages start at 1, sorted by update time descending, then ID. For example,
|
|
279
|
+
`/memory list legacy 2` reaches the next 20 imports. `all` is retained as an alias;
|
|
280
|
+
`here` and `legacy` are optional inspection filters, never recall settings. There is no
|
|
281
|
+
full export command. Concurrent updates can move records between
|
|
282
|
+
pages. Lists can show conflicted or stale project-state records that search/recall
|
|
283
|
+
excludes. Status counts include all scopes and tombstones and validate source jobs and
|
|
284
|
+
history as well as memory records. Long content previews are capped at 1,440 bytes in
|
|
285
|
+
lists/search or 8,000 bytes in `show`, with an ellipsis when truncated.
|
|
286
|
+
Commands that take exact IDs can address records outside the current cwd. `search` and
|
|
287
|
+
`explain <query>` use only their explicit query, whereas automatic recall can resolve
|
|
288
|
+
follow-ups from recent user context. `explain` without arguments shows the last automatic
|
|
289
|
+
snapshot: normalized focus/context features, eligible/excluded counts, scores, coverage,
|
|
290
|
+
up to 10 candidate IDs and rejection/selection reasons, plus actual injected count/bytes.
|
|
291
|
+
It retains at most 8,000 bytes (+ truncation marker) in memory, not a database/session log;
|
|
292
|
+
no memory bodies or provider errors are included. It resets on reload and is not proof
|
|
293
|
+
of what the model subsequently understood. Empty/no-match turns replace the old snapshot. Pin protects against automatic replacement/age expiry,
|
|
294
|
+
not manual edits, and does not force an unrelated record into every prompt. Pin/unpin
|
|
295
|
+
and adoption, feedback and conflict resolution preserve the stored evidence date; undo restores the prior date. These
|
|
296
|
+
bookkeeping actions do not restart the seven-day project-state recall window. Alias-only
|
|
297
|
+
model enrichment also preserves that date. A new, explicitly incorporated progress
|
|
298
|
+
observation can refresh it, even if the observed state is still unchanged/pending.
|
|
299
|
+
Manual correction clears old search aliases rather than attaching them to new content.
|
|
300
|
+
|
|
301
|
+
Undo reverses claim changes only when the affected records have not changed since;
|
|
302
|
+
it is not a database rollback. Suppression hashes remain, and jobs are not reopened.
|
|
303
|
+
Suppression retires pending sources known to repeat that fact, not just its first parent.
|
|
304
|
+
This skips the entire pending model pass for those sources; unrelated local claims remain,
|
|
305
|
+
but unlearned prose may need to be restated in a new source. Job state `done` also includes
|
|
306
|
+
these retired sources; history identifies actual model transactions.
|
|
307
|
+
There is no `/memory confirm`, `/evolution approve`, or owner-approval step in 0.2.
|
|
308
|
+
|
|
309
|
+
## Migration from 0.1
|
|
310
|
+
|
|
311
|
+
On first database use, valid `memories.jsonl` and `memory-actions.jsonl` are imported
|
|
312
|
+
once in a transaction. **Original files are not modified or deleted.** Corrupt or
|
|
313
|
+
unreadable ledgers stop migration rather than silently ignoring forget/correct actions.
|
|
314
|
+
|
|
315
|
+
Old records without origin metadata retain the label `legacy`. They participate in
|
|
316
|
+
relevance-based recall without adoption, with their unknown origin visible. Use
|
|
317
|
+
`/memory list legacy [page]` for inspection; `/memory adopt <id>` is an optional metadata
|
|
318
|
+
annotation, not a prerequisite for recall. Structured claims are extracted from eligible
|
|
319
|
+
legacy summaries, respecting existing lifecycle actions. Free-form raw summaries
|
|
320
|
+
remain available in the original JSONL but are not injected as claims. New imports
|
|
321
|
+
preserve unchanged children of corrected summaries and carry superseded-content hashes
|
|
322
|
+
when corrected legacy claims are adopted into a project.
|
|
323
|
+
|
|
324
|
+
**Upgrading from earlier 0.2 development builds:** stop Pi and back up the state directory
|
|
325
|
+
first, then update/reload all Pi processes sharing it. Schema markers **2, 3 and 4 upgrade
|
|
326
|
+
transactionally to 5**, adding replay-safe feedback receipts and the optional evidence/feedback
|
|
327
|
+
contract (plus missing retry fields for older schemas). Existing records, IDs, histories and
|
|
328
|
+
evidence dates remain unchanged. Missing evidence stays unknown, with no fabricated backfill.
|
|
329
|
+
Existing failures below the limit become automatically eligible; their old error cause/time
|
|
330
|
+
remain labeled unknown rather than invented. No copying, manual marker reset or JSONL
|
|
331
|
+
re-import is needed. Older builds reject schema 5; rollback requires a matching backup,
|
|
332
|
+
not editing the marker. Existing records are immediately eligible for global relevance-based recall.
|
|
333
|
+
Legacy claims previously excluded by cwd filtering become eligible too. This shares
|
|
334
|
+
relevant stored claims with the active Pi session/provider, not raw session archives.
|
|
335
|
+
|
|
336
|
+
These migration fixes do not replay an already completed import or retroactively erase
|
|
337
|
+
previously stored sensitive data. If an older import already lost revision information,
|
|
338
|
+
use the preserved ledgers/backup to review and correct affected records; do not reset the
|
|
339
|
+
migration marker or replace a populated database blindly.
|
|
340
|
+
|
|
341
|
+
Old signals, agenda, thresholds, proposals, journals and execution plans are historical
|
|
342
|
+
files only. This version neither processes nor deletes them. Back up the entire state
|
|
343
|
+
directory with Pi stopped before changing versions. Returning to 0.1 reads the old
|
|
344
|
+
JSONL, not changes made in the new database.
|
|
345
|
+
|
|
346
|
+
## Recovery and troubleshooting
|
|
347
|
+
|
|
348
|
+
- **No `/memory` command:** check `pi list`, then `/reload` (or restart Pi). Only one
|
|
349
|
+
source of this extension should be installed. Processes marked by a nonempty
|
|
350
|
+
`PI_SUBAGENT_AGENT_ID` deliberately do not load it.
|
|
351
|
+
- **No recalled memories:** check the active agent directory and `/memory search <topic>`.
|
|
352
|
+
Name the topic if the current session has no recent user context; cwd is not a recall
|
|
353
|
+
gate. `/memory explain` shows the last automatic decision; `explain <query>` previews
|
|
354
|
+
an explicit query without invoking a model or updating claims. Forgotten/conflicted records and unpinned project state older than seven days
|
|
355
|
+
are not recalled. Matching remains lexical, not a guarantee of semantic or cross-language
|
|
356
|
+
equivalence; vague references and languages/terms outside the concept map or learned
|
|
357
|
+
aliases can still be missed.
|
|
358
|
+
- **Old progress still shown:** `/memory learning` distinguishes no work observation,
|
|
359
|
+
no update targets, already captured and newly captured sources, with retained/omitted
|
|
360
|
+
observation counts and nomination reasons. `/memory status` shows actual changed-record
|
|
361
|
+
counts from recent model transactions: `done` means processed/retired, **not learned**.
|
|
362
|
+
Age/weight does not prove a task finished. A new eligible
|
|
363
|
+
work observation or compaction can update tracked progress; upgrading alone does not
|
|
364
|
+
invent completion or replay old tool transcripts. Inspect `/memory show <id>` and
|
|
365
|
+
history, or correct the record explicitly when you know the current state.
|
|
366
|
+
- **Pending/failed jobs:** normally no command is needed: leave Pi running and recovery
|
|
367
|
+
automatically processes due work. `/memory status` distinguishes scheduled retries from
|
|
368
|
+
paused sources and shows the next eligible time (the next poll may be up to 15 seconds
|
|
369
|
+
later when no local work is queued). After a crash, the 150-second lease must expire first.
|
|
370
|
+
If a source reaches 5 failures, inspect its category: `timeout`, `output_limit`,
|
|
371
|
+
`invalid_output`, `write_rejected`, `stale`, `provider`, `unavailable`, `interrupted` or
|
|
372
|
+
`unknown`. Fix model/authentication or investigate repeated output/write rejection;
|
|
373
|
+
`/memory evolve` can then make one extra attempt without resetting the automatic budget.
|
|
374
|
+
A bad source remains saved/visible rather than being deleted or labeled successful.
|
|
375
|
+
- **Persistent storage/import errors:** stop all Pi processes using that agent directory
|
|
376
|
+
and back up the **entire** state directory, including any SQLite sidecars and legacy
|
|
377
|
+
ledgers. Check file permissions and restore a known-good matching backup if needed.
|
|
378
|
+
Do not delete `memory-actions.jsonl` to bypass an import failure: that can discard
|
|
379
|
+
forget/correct history. Failed imports can retry after repairing the original ledgers;
|
|
380
|
+
successful imports are never replayed merely because the old JSONL changes.
|
|
381
|
+
- **Disable the extension:** `pi remove <installed-source>`, then `/reload`. Its local
|
|
382
|
+
database remains on disk. Restore/replace state only with all processes using it
|
|
383
|
+
stopped; do not mix one backup's database with another's WAL/SHM files.
|
|
384
|
+
|
|
385
|
+
Diagnostics intentionally do not echo provider error bodies, which may contain secrets.
|
|
386
|
+
`SQLite ok (schema 5)` checks database structure/record validity, not the truth of model claims.
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-memory-evolution",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Automatic cross-session memory using Pi's active model, transactional local storage, and topic-based recall across directories.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/btnalit/pi-memory-evolution.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/btnalit/pi-memory-evolution",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/btnalit/pi-memory-evolution/issues"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.19"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi",
|
|
25
|
+
"pi-agent",
|
|
26
|
+
"memory",
|
|
27
|
+
"self-evolution"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"!src/**/*.test.ts",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"package.json"
|
|
37
|
+
],
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./src/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
+
"typebox": "*"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test:pi": "node scripts/test-pi.mjs",
|
|
51
|
+
"test:install": "node scripts/test-install.mjs",
|
|
52
|
+
"check": "npm run typecheck && npm test && npm run check:package",
|
|
53
|
+
"check:package": "node scripts/check-package.mjs"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
57
|
+
"@types/node": "22.18.0",
|
|
58
|
+
"typescript": "5.9.3",
|
|
59
|
+
"typebox": "1.3.7"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
export interface OperationResource { path: string; name: string; kind: 'directory' | 'file' }
|
|
6
|
+
/** Small shell lexer for hints, not execution or a success verifier. Quoted semicolons
|
|
7
|
+
* and heredoc bodies cannot manufacture separate commands. Unsupported syntax loses hints. */
|
|
8
|
+
export function shellCommands(command: string): string[][] {
|
|
9
|
+
const result: string[][] = []; let words: string[] = [], word = '', quote = '', escaped = false;
|
|
10
|
+
const flushWord = () => { if (word) words.push(word); word = ''; };
|
|
11
|
+
const flush = () => { flushWord(); if (words.length) result.push(words); words = []; };
|
|
12
|
+
for (let i = 0; i < Math.min(command.length, 8192); i++) {
|
|
13
|
+
const c = command[i];
|
|
14
|
+
if (escaped) { word += c; escaped = false; continue; }
|
|
15
|
+
if (c === '\\' && quote !== "'") { escaped = true; continue; }
|
|
16
|
+
if (quote) { if (c === quote) quote = ''; else word += c; continue; }
|
|
17
|
+
if (c === '"' || c === "'") { quote = c; continue; }
|
|
18
|
+
if (c === '<' && command[i+1] === '<') { flush(); return result; }
|
|
19
|
+
if (';|&\n'.includes(c)) { flush(); continue; }
|
|
20
|
+
if (/\s/u.test(c)) { flushWord(); continue; }
|
|
21
|
+
word += c;
|
|
22
|
+
}
|
|
23
|
+
if (!quote && !escaped) flush();
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commandWords(input: string[]): string[] {
|
|
28
|
+
const words = [...input];
|
|
29
|
+
if (words[0] === 'env') {
|
|
30
|
+
words.shift();
|
|
31
|
+
while (['-i','--ignore-environment','--'].includes(words[0])) words.shift();
|
|
32
|
+
}
|
|
33
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(words[0] ?? '')) words.shift();
|
|
34
|
+
return words;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function operationPriority(tool: string, args: string): number {
|
|
38
|
+
if (tool === 'write' || tool === 'edit') return 3;
|
|
39
|
+
if (tool !== 'bash') return 0;
|
|
40
|
+
let priority = 0;
|
|
41
|
+
for (const raw of shellCommands(args)) {
|
|
42
|
+
const words = commandWords(raw);
|
|
43
|
+
const executable = basename(words[0] ?? '');
|
|
44
|
+
if (executable === 'git') {
|
|
45
|
+
if (words.some(w => ['commit','push','merge','rebase','cherry-pick'].includes(w))) priority = Math.max(priority, 5);
|
|
46
|
+
else if (words.some(w => ['status','log','show','diff','ls-remote'].includes(w))) priority = Math.max(priority, 4);
|
|
47
|
+
else priority = Math.max(priority, 3);
|
|
48
|
+
} else if (['npm','pnpm','yarn','bun','node','python','python3','pytest','cargo','go','make','cmake','tsc'].includes(executable)) priority = Math.max(priority, 4);
|
|
49
|
+
else if (['systemctl','docker','podman','kubectl','pip','pip3','pacman','install','cp','mv','rm','mkdir','touch'].includes(executable)) priority = Math.max(priority, 3);
|
|
50
|
+
}
|
|
51
|
+
return priority;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resource(value: string, cwd: string, directory: boolean): OperationResource | undefined {
|
|
55
|
+
if (!value || /[$`*?(){}\n\r]/u.test(value) || value.startsWith('-')) return;
|
|
56
|
+
let path = resolve(cwd, value.startsWith('~/') ? join(homedir(), value.slice(2)) : value);
|
|
57
|
+
try { path = realpathSync(path); } catch { /* Synthetic/nonexistent resources retain literal identity. */ }
|
|
58
|
+
// A file in a real checkout carries its repository identity, without a shell call.
|
|
59
|
+
if (!directory) {
|
|
60
|
+
let parent = dirname(path);
|
|
61
|
+
for (let i = 0; i < 16; i++) {
|
|
62
|
+
if (existsSync(join(parent, '.git'))) { path = parent; directory = true; break; }
|
|
63
|
+
const next = dirname(parent); if (next === parent) break; parent = next;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { path, name: basename(path), kind: directory ? 'directory' : 'file' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Only operation arguments supply resources; never tool output, source origins or cwd
|
|
70
|
+
* by itself. An explicit cd/git -C is a directory hint, not proof a command succeeded. */
|
|
71
|
+
export function operationResources(tool: string, args: string, cwd: string): OperationResource[] {
|
|
72
|
+
if (tool === 'write' || tool === 'edit' || tool === 'read') {
|
|
73
|
+
const found = resource(args, cwd, false); return found ? [found] : [];
|
|
74
|
+
}
|
|
75
|
+
if (tool !== 'bash') return [];
|
|
76
|
+
const result: OperationResource[] = []; let directory = cwd;
|
|
77
|
+
for (const raw of shellCommands(args)) {
|
|
78
|
+
const words = commandWords(raw);
|
|
79
|
+
const executable = basename(words[0] ?? '');
|
|
80
|
+
const index = executable === 'cd' ? 1 : executable === 'git' ? words.indexOf('-C') + 1 : 0;
|
|
81
|
+
if (!index || !words[index]) continue;
|
|
82
|
+
const found = resource(words[index], directory, true);
|
|
83
|
+
if (found) { result.push(found); if (executable === 'cd') directory = found.path; }
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isInternalObservation(tool: string, args: string, stateDir?: string): boolean {
|
|
89
|
+
if (/^(?:memory_|memory$)/u.test(tool)) return true;
|
|
90
|
+
if (!stateDir) return false;
|
|
91
|
+
// Avoid feeding this extension's own database/diagnostics back as independent evidence.
|
|
92
|
+
const normalized = args.replaceAll('\\', '/');
|
|
93
|
+
const path = resolve(stateDir).replaceAll('\\', '/');
|
|
94
|
+
return new RegExp(path.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') + `(?=$|[\\s/"';)])`, 'u').test(normalized);
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { EVOLUTION_MAX_TOKENS, EvolutionError } from "../memory/recovery.ts";
|
|
4
|
+
|
|
5
|
+
export interface Completion { text: string; model: string }
|
|
6
|
+
export type CompleteMemory = (ctx: ExtensionContext, systemPrompt: string, input: string, signal: AbortSignal) => Promise<Completion>;
|
|
7
|
+
|
|
8
|
+
/** Pi 0.85 public model facade reuses the active model, provider composition and auth. */
|
|
9
|
+
export const completeMemory: CompleteMemory = async (ctx, systemPrompt, input, signal) => {
|
|
10
|
+
const model = ctx.model;
|
|
11
|
+
if (!model) throw new EvolutionError("unavailable");
|
|
12
|
+
const modelId = `${model.provider}/${model.id}`;
|
|
13
|
+
// Feature check allows old Pi to fall back to local extraction.
|
|
14
|
+
const registry = ctx.modelRegistry;
|
|
15
|
+
if (typeof registry.complete !== "function") throw new EvolutionError("unavailable");
|
|
16
|
+
const maxTokens = Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0
|
|
17
|
+
? Math.min(EVOLUTION_MAX_TOKENS, model.maxTokens) : EVOLUTION_MAX_TOKENS;
|
|
18
|
+
const response = await registry.complete(model, {
|
|
19
|
+
systemPrompt,
|
|
20
|
+
messages: [{ role: "user", content: input, timestamp: Date.now() }],
|
|
21
|
+
}, { signal, maxTokens, cacheRetention: "none", sessionId: randomUUID() });
|
|
22
|
+
if (response.stopReason !== "stop") throw new EvolutionError(response.stopReason === "length" ? "output_limit" : "provider");
|
|
23
|
+
return { model: modelId, text: response.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n") };
|
|
24
|
+
};
|