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/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
+ };