pi-session-memory 0.2.1 → 0.3.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/README.md CHANGED
@@ -17,7 +17,7 @@ A local-first Pi extension that saves completed conversations to SQLite and give
17
17
  - Ranks results by literal relevance and recency, boosts an explicitly scoped project, and limits results to two turns per session for diversity.
18
18
  - Supports explicit durable memories, which remain after their source transcript turns are deleted.
19
19
  - Suppresses a raw turn from recall when an active durable memory contains the same unchanged pinned source evidence; other turns in that session remain eligible.
20
- - Flags a recalled durable memory when a newer matching turn exists in its source session, so users can explicitly confirm or supersede it without silent updates.
20
+ - Associates recalled durable memories with later source-session activity and newer query-relevant evidence from the same or another session, so users can explicitly compare, confirm, or supersede them without silent updates.
21
21
  - Includes status, direct search, and permanent deletion commands so users can inspect and control local memory.
22
22
  - Uses only Node.js built-ins and SQLite (`node:sqlite`); no external runtime dependencies.
23
23
 
@@ -46,7 +46,7 @@ pi -e npm:pi-session-memory
46
46
  To intentionally pin a known version (which `pi update --extensions` skips), add its version explicitly:
47
47
 
48
48
  ```bash
49
- pi install npm:pi-session-memory@0.2.1
49
+ pi install npm:pi-session-memory@0.3.0
50
50
  ```
51
51
 
52
52
  ## Usage
@@ -108,7 +108,7 @@ What did we decide about LangGraph last time?
108
108
 
109
109
  `recall_memory` is the discovery step: it searches and ranks the complete active durable-memory and raw-turn match set using the original request plus important entities. It supports optional exact project-directory, source, and time-window filters. Each tool response deliberately renders five results and reports `totalResults` and `nextOffset`; when more candidates are needed, Pi repeats the exact same query and filters with that explicit offset. This pages model context without silently limiting the local search. Raw transcript candidates contain a short excerpt plus a session ID and turn index, rather than the entire turn context. When surrounding conversation is needed to answer accurately, Pi calls `fetch_session` with that session ID and the smallest useful turn-index range. When an initial literal search is empty, Pi may make up to two additional local searches using reasoned alternatives—such as abbreviations, expansions, aliases, translations, or likely task wording—while retaining the original filters.
110
110
 
111
- When recall returns a durable memory, Pi is instructed to naturally communicate a relevant remembered conclusion and provenance when useful. If newer matching evidence makes that memory a freshness candidate, Pi explains the discrepancy and asks whether you want to keep, confirm, or replace it. It never claims a memory was updated or superseded without your explicit choice.
111
+ When recall returns a durable memory, Pi is instructed to naturally communicate a relevant remembered conclusion and provenance when useful. It reports later activity in the memory's source session separately from newer query-relevant evidence to compare; that evidence can come from the original session or another newer session. Pi compares the old memory with the evidence as a possible confirmation, supplement, conflict, or replacement, then asks whether you want to keep, confirm, or replace it. It never claims a memory was updated or superseded without your explicit choice.
112
112
 
113
113
  ### Inspect and control memory
114
114
 
@@ -132,7 +132,7 @@ Recall and freshness explanations are automatic model behavior. The commands bel
132
132
  - `/remember <text>` saves an explicit durable `fact` scoped to the current project.
133
133
  - `/memory-pin <turn-id>` promotes a historical turn to a durable fact and records its source session, source turn ID, and a hash of the pinned evidence.
134
134
  - `/memory-list [kind]` displays durable memories, optionally limited to `preference`, `decision`, `fact`, `project_state`, `task`, or `lesson`.
135
- - Recall marks a durable memory as a freshness candidate when a newer query-matching turn appears in its source session; this is a review signal, not an automatic update.
135
+ - Recall distinguishes later activity in a memory's source session from newer query-relevant evidence to compare. That evidence may come from the original session or another newer session; it is a review signal, not an automatic update.
136
136
  - `/memory-confirm <memory-id>` records that an active memory remains current by updating `last_confirmed_at`.
137
137
  - `/memory-supersede <old-memory-id> <new-memory-id>` explicitly replaces an active memory while retaining the old record for history; superseded memories are excluded from normal recall.
138
138
  - `/memory-history <memory-id>` displays the complete oldest-to-newest supersession chain.
@@ -168,6 +168,7 @@ Conversation data is stored and queried locally. This package does not add a rem
168
168
 
169
169
  | Version | Highlights |
170
170
  | --- | --- |
171
+ | `0.3.0` | Replaces source-session-only freshness hints with provenance-linked evidence comparison across newer same-session and cross-session turns. This changes recall output and `freshness_candidate` semantics, but keeps tool inputs, slash commands, SQLite data, and explicit user-controlled memory mutation compatible; no migration is required. |
171
172
  | `0.2.1` | Pages `recall_memory` results in explicit five-result `offset` windows while still evaluating the complete local match set; npm publishing now uses a runtime-file allowlist. |
172
173
  | `0.2.0` | Added cross-client SQLite recall and durable-memory controls, plus native current-project Codex-to-Pi session migration for `/resume`. |
173
174
  | `0.1.4` | Automatically syncs new or changed Pi, Claude Code, and Codex history when Pi starts; `/memory-backfill` forces a full rescan. |
@@ -176,6 +177,18 @@ Conversation data is stored and queried locally. This package does not add a rem
176
177
  | `0.1.1` | Added repository and package metadata for public distribution. |
177
178
  | `0.1.0` | Initial release: local SQLite memory, Pi live persistence, historical import, and `recall_memory` retrieval. |
178
179
 
180
+ ## Release compatibility review
181
+
182
+ Before every significant release, review these compatibility surfaces and record any migration or versioning decision:
183
+
184
+ 1. **Install/package:** package name, Pi manifest, runtime dependencies, and published file allowlist.
185
+ 2. **Persistent data:** SQLite schema/migrations, JSONL-import compatibility, and any data rewrite.
186
+ 3. **Agent tools and commands:** tool names, input schemas, result/details contracts, and slash commands.
187
+ 4. **Retrieval and agent behavior:** ranking, pagination, freshness/evidence semantics, prompt policy, and automatic side effects.
188
+ 5. **Public TypeScript/module API:** exported types/functions and required result fields.
189
+
190
+ The `0.3.0` review found no installation, SQLite, command, or tool-input breaking change. It intentionally changes recall result semantics and adds evidence fields, so it is released as a minor `0.x` version rather than a patch.
191
+
179
192
  ## Development
180
193
 
181
194
  ```bash
@@ -211,9 +211,9 @@ Invocation policy:
211
211
 
212
212
  Memory-aware response policy:
213
213
  1. Treat returned durable memories as reusable evidence, not as invisible context. In your natural-language answer, briefly state the relevant remembered conclusion and identify its source turn/session when that provenance matters to the answer.
214
- 2. If a durable memory is marked **Freshness**, explain that newer matching evidence exists. Compare the old memory and the newer raw evidence; do not claim that the memory was updated, confirmed, or superseded unless the user explicitly chose that action.
215
- 3. After explaining a meaningful freshness conflict or decision change, offer the user clear control: keep the current memory, confirm that it remains current, or create/pin a replacement and supersede the old memory. Ask which outcome they want before any persistent memory-management action.
216
- 4. When an existing durable memory resolves the question and has no freshness signal, use it directly and avoid repeating its identical source turn. Do not mention memory mechanics unless provenance or freshness is useful to the user.
214
+ 2. A durable memory can report `source_session_changed` when its original session has later activity; this alone does not mean the memory is stale. When it includes **Newer evidence to compare**, compare that evidence with the memory: it may confirm, supplement, conflict with, or replace the old conclusion. Evidence can come from another newer session as well as the original session. Do not claim that the memory was updated, confirmed, or superseded unless the user explicitly chose that action.
215
+ 3. After explaining a meaningful comparison, offer clear control: keep the current memory, confirm that it remains current, or create/pin a replacement and supersede the old memory. Ask which outcome they want before any persistent memory-management action.
216
+ 4. When an existing durable memory resolves the question and has no comparison evidence, use it directly and avoid repeating its identical source turn. Do not mention memory mechanics unless provenance or evidence comparison is useful to the user.
217
217
  5. Slash commands are user-controlled management actions. Do not instruct the user to execute a command merely to answer their question; mention the relevant command only when they want to inspect, confirm, replace, or delete a memory.
218
218
 
219
219
  Session-expansion policy:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-session-memory",
3
- "version": "0.2.1",
4
- "description": "Persistent, local-first cross-session memory for Pi, with SQLite-backed paginated recall across Pi, Claude Code, and Codex conversations",
3
+ "version": "0.3.0",
4
+ "description": "Persistent, local-first cross-session memory for Pi, with SQLite-backed paginated recall and freshness-evidence comparison across Pi, Claude Code, and Codex conversations",
5
5
  "keywords": [
6
6
  "pi-package"
7
7
  ],
package/src/retriever.ts CHANGED
@@ -26,6 +26,16 @@ export interface RecallTurnResult {
26
26
  score: number;
27
27
  }
28
28
 
29
+ export interface FreshnessEvidence {
30
+ turn_id: string;
31
+ session_id: string;
32
+ turn_index: number;
33
+ source: MemorySource;
34
+ ts: number;
35
+ relation: "same_source_session_later" | "newer_cross_session";
36
+ excerpt: string;
37
+ }
38
+
29
39
  export interface RecallDurableMemoryResult {
30
40
  type: "memory";
31
41
  memory_id: string;
@@ -36,7 +46,9 @@ export interface RecallDurableMemoryResult {
36
46
  source_session_id: string | null;
37
47
  source_content_hash: string | null;
38
48
  source_turn_index: number | null;
49
+ source_session_changed: boolean;
39
50
  freshness_candidate: boolean;
51
+ freshness_evidence: FreshnessEvidence[];
40
52
  created_at: number;
41
53
  last_confirmed_at: number;
42
54
  importance: number;
@@ -67,13 +79,15 @@ export function recallTurns(entities: string[]): RecallTurnResult[] {
67
79
  export function recallMemories(options: RecallOptions): RecallResult[] {
68
80
  const durableMemories = _recallDurableMemories(options);
69
81
  const recalledTurns = _recallTurns(options);
70
- const freshnessCandidates = new Set(
71
- durableMemories
72
- .filter((memory) => memory.source_session_id && memory.source_turn_index !== null)
73
- .filter((memory) => recalledTurns.some((turn) => turn.session_id === memory.source_session_id && turn.turn_index > memory.source_turn_index!))
74
- .map((memory) => memory.memory_id),
75
- );
76
- const memories = durableMemories.map((memory) => ({ ...memory, freshness_candidate: freshnessCandidates.has(memory.memory_id) }));
82
+ const memories = durableMemories.map((memory) => {
83
+ const freshness_evidence = _freshnessEvidence(memory, recalledTurns);
84
+ return {
85
+ ...memory,
86
+ source_session_changed: _sourceSessionChanged(memory),
87
+ freshness_candidate: freshness_evidence.length > 0,
88
+ freshness_evidence,
89
+ };
90
+ });
77
91
  const coveredSourceHashes = new Map(
78
92
  memories
79
93
  .filter((memory) => memory.source_turn_id && memory.source_content_hash)
@@ -105,7 +119,14 @@ export function formatRecallResults(results: RecallResult[], options?: Pick<Reca
105
119
  lines.push(`**Memory ID:** ${result.memory_id}`);
106
120
  if (result.source_turn_id) lines.push(`**Source turn:** ${result.source_turn_id}`);
107
121
  if (result.source_session_id) lines.push(`**Source session:** ${result.source_session_id}`);
108
- if (result.freshness_candidate) lines.push("**Freshness:** newer matching turn exists in the source session; confirm or supersede this memory.");
122
+ if (result.source_session_changed) lines.push("**Source session changed:** later turns exist; this alone does not mean the memory is stale.");
123
+ if (result.freshness_candidate) {
124
+ lines.push("**Newer evidence to compare:**");
125
+ for (const evidence of result.freshness_evidence) {
126
+ lines.push(`- [${evidence.relation.replaceAll("_", " ")} · ${evidence.source} · ${new Date(evidence.ts).toLocaleString()} · ${evidence.session_id} · turn ${evidence.turn_index}] ${evidence.excerpt}`);
127
+ }
128
+ lines.push("Compare this evidence with the durable memory; it may confirm, supplement, conflict with, or replace it. Do not change the memory without the user's explicit choice.");
129
+ }
109
130
  } else {
110
131
  const date = new Date(result.ts).toLocaleString();
111
132
  lines.push(`### [${result.source} · ${date}]`);
@@ -167,7 +188,45 @@ function _recallDurableMemories(options: RecallOptions): RecallDurableMemoryResu
167
188
  WHERE ${filters.join(" AND ")}
168
189
  ORDER BY hits DESC, importance DESC, last_confirmed_at DESC
169
190
  `).all(...parameters, ...filterParameters)
170
- .map((memory) => ({ ...memory, type: "memory" as const, freshness_candidate: false, score: memory.hits + memory.importance })) as RecallDurableMemoryResult[];
191
+ .map((memory) => ({ ...memory, type: "memory" as const, source_session_changed: false, freshness_candidate: false, freshness_evidence: [], score: memory.hits + memory.importance })) as RecallDurableMemoryResult[];
192
+ }
193
+
194
+ /** Detect later activity in the original session independently of this recall query and ranking. */
195
+ function _sourceSessionChanged(memory: RecallDurableMemoryResult): boolean {
196
+ if (!memory.source_session_id || memory.source_turn_index === null) return false;
197
+ return getDb().prepare(`
198
+ SELECT 1 FROM turns
199
+ WHERE session_id = ? AND turn_index > ?
200
+ LIMIT 1
201
+ `).get(memory.source_session_id, memory.source_turn_index) !== undefined;
202
+ }
203
+
204
+ /** Associate each memory with all newer query-relevant turns without deciding their semantic relationship. */
205
+ function _freshnessEvidence(memory: RecallDurableMemoryResult, recalledTurns: RecallTurnResult[]): FreshnessEvidence[] {
206
+ const baselineTs = _memoryEvidenceTimestamp(memory);
207
+ return recalledTurns
208
+ .filter((turn) => {
209
+ if (memory.source_session_id === turn.session_id && memory.source_turn_index !== null) {
210
+ return turn.turn_index > memory.source_turn_index;
211
+ }
212
+ return turn.ts > baselineTs;
213
+ })
214
+ .map((turn) => ({
215
+ turn_id: turn.turn_id,
216
+ session_id: turn.session_id,
217
+ turn_index: turn.turn_index,
218
+ source: turn.source,
219
+ ts: turn.ts,
220
+ relation: memory.source_session_id === turn.session_id ? "same_source_session_later" as const : "newer_cross_session" as const,
221
+ excerpt: _excerpt(turn.user_text || turn.reply_text),
222
+ }));
223
+ }
224
+
225
+ /** Use source-turn time when available; explicit memories become comparable from their creation time. */
226
+ function _memoryEvidenceTimestamp(memory: RecallDurableMemoryResult): number {
227
+ if (!memory.source_turn_id) return memory.created_at;
228
+ const source = getDb().prepare("SELECT ts FROM turns WHERE turn_id = ?").get(memory.source_turn_id) as { ts: number } | undefined;
229
+ return source?.ts ?? memory.created_at;
171
230
  }
172
231
 
173
232
  /** Retrieve and rank locally stored turns using literal query terms and optional scopes. */