pi-session-memory 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/extensions/index.ts +20 -19
- package/package.json +2 -2
- package/src/retriever.ts +81 -23
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
|
-
-
|
|
20
|
+
- Associates recalled durable memories with later source-session activity and newer entity-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.
|
|
49
|
+
pi install npm:pi-session-memory@0.3.1
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Usage
|
|
@@ -106,9 +106,9 @@ The extension instructs Pi to call `recall_memory` when a user explicitly asks a
|
|
|
106
106
|
What did we decide about LangGraph last time?
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
`recall_memory` is the discovery step: it searches and ranks the complete active durable-memory and raw-turn match set using the
|
|
109
|
+
`recall_memory` is the discovery step: it searches and ranks the complete active durable-memory and raw-turn match set using 2–8 high-signal literal entities. Entities are OR alternatives, so a result may match any entity; matching more entities ranks higher. Pi must not send the complete user request or generic conversational terms such as "问题", "开发", "有哪些", or "help". Where useful, it includes Chinese/English equivalents, aliases, or abbreviations—for example `["bug", "缺陷", "错误", "fix", "修复"]`. Exact project-directory, source, and time-window filters remain strict scope constraints. Each tool response deliberately renders five results and reports `totalResults` and `nextOffset`; when more candidates are needed, Pi repeats the exact same entities and scope 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 distinct entities selected from reasoned alternatives—such as abbreviations, expansions, aliases, translations, or likely task wording—while retaining the original scope filters.
|
|
110
110
|
|
|
111
|
-
When recall returns a durable memory, Pi is instructed to naturally communicate a relevant remembered conclusion and provenance when useful.
|
|
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 entity-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
|
|
135
|
+
- Recall distinguishes later activity in a memory's source session from newer entity-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,8 @@ 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.1` | Makes `recall_memory` entity-only: 2–8 high-signal literal entities are OR alternatives, results matching more entities rank higher, and output distinguishes search entities from strict scope filters. Existing SQLite data and schema remain compatible; no migration is required. |
|
|
172
|
+
| `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 SQLite data and explicit user-controlled memory mutation compatible; no migration is required. |
|
|
171
173
|
| `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
174
|
| `0.2.0` | Added cross-client SQLite recall and durable-memory controls, plus native current-project Codex-to-Pi session migration for `/resume`. |
|
|
173
175
|
| `0.1.4` | Automatically syncs new or changed Pi, Claude Code, and Codex history when Pi starts; `/memory-backfill` forces a full rescan. |
|
|
@@ -176,8 +178,23 @@ Conversation data is stored and queried locally. This package does not add a rem
|
|
|
176
178
|
| `0.1.1` | Added repository and package metadata for public distribution. |
|
|
177
179
|
| `0.1.0` | Initial release: local SQLite memory, Pi live persistence, historical import, and `recall_memory` retrieval. |
|
|
178
180
|
|
|
181
|
+
## Release compatibility review
|
|
182
|
+
|
|
183
|
+
Before every significant release, review these compatibility surfaces and record any migration or versioning decision. In this project, a breaking change means an updated extension conflicts with a user's existing SQLite table structure and errors after upgrade; changes to agent tool inputs or external direct callers are not breaking changes under this definition:
|
|
184
|
+
|
|
185
|
+
1. **Install/package:** package name, Pi manifest, runtime dependencies, and published file allowlist.
|
|
186
|
+
2. **Persistent data:** SQLite schema/migrations, JSONL-import compatibility, and any data rewrite.
|
|
187
|
+
3. **Agent tools and commands:** tool names, input schemas, result/details contracts, and slash commands.
|
|
188
|
+
4. **Retrieval and agent behavior:** ranking, pagination, freshness/evidence semantics, prompt policy, and automatic side effects.
|
|
189
|
+
5. **Public TypeScript/module API:** exported types/functions and required result fields.
|
|
190
|
+
6. **Extension loadability:** run `npm test`, which imports `extensions/index.ts`; Markdown inline-code backticks inside a template-literal description must be escaped as `\`` so Pi can parse and start the extension.
|
|
191
|
+
|
|
192
|
+
The `0.3.1` review found no SQLite breaking change: it does not alter tables, migrations, or stored data, so users can upgrade without a database error or migration. It is therefore released as a patch despite changing `recall_memory` search inputs and behavior.
|
|
193
|
+
|
|
179
194
|
## Development
|
|
180
195
|
|
|
181
196
|
```bash
|
|
182
197
|
npm test
|
|
183
198
|
```
|
|
199
|
+
|
|
200
|
+
The test suite imports `extensions/index.ts` in addition to exercising core behavior. This catches extension-load syntax errors, including unescaped Markdown backticks inside template-literal tool descriptions.
|
package/extensions/index.ts
CHANGED
|
@@ -62,8 +62,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
62
|
handler: async (args, ctx) => {
|
|
63
63
|
const query = args.trim();
|
|
64
64
|
if (!query) throw new Error("Usage: /memory-search <query>");
|
|
65
|
-
const
|
|
66
|
-
|
|
65
|
+
const entities = [query];
|
|
66
|
+
const page = paginateRecallResults(recallMemories({ entities }));
|
|
67
|
+
ctx.ui.notify(formatRecallResults(page.results, { entities }, page), "info");
|
|
67
68
|
},
|
|
68
69
|
});
|
|
69
70
|
|
|
@@ -196,7 +197,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
197
|
pi.registerTool({
|
|
197
198
|
name: "recall_memory",
|
|
198
199
|
label: "Recall Memory",
|
|
199
|
-
description: `Search this user's past conversation history across Pi, Claude Code, and Codex.
|
|
200
|
+
description: `Search this user's past conversation history across Pi, Claude Code, and Codex using high-signal literal entities. Entities are OR alternatives: any entity may recall a result, and results matching more entities rank higher. Scope filters only narrow the local search.
|
|
200
201
|
|
|
201
202
|
Invocation policy:
|
|
202
203
|
1. Call this tool immediately when the user explicitly asks to review, remember, summarize, continue, or compare a previous discussion about a topic. Examples:
|
|
@@ -206,14 +207,15 @@ Invocation policy:
|
|
|
206
207
|
- "之前那个方案怎么说的" / "what was that plan we had?"
|
|
207
208
|
2. When you cannot confidently answer from the current conversation and your general knowledge, but the user may have discussed the topic in prior sessions, first ask whether they want you to search their conversation history. Call this tool only after they agree.
|
|
208
209
|
3. Do not search history merely because a question is difficult when the user's prior discussions are not relevant.
|
|
209
|
-
4.
|
|
210
|
-
5.
|
|
210
|
+
4. Use 2–8 specific, high-signal literal entities, not the user's entire request or generic conversational words such as "问题", "开发", "有哪些", or "help". Include useful aliases, abbreviations, or Chinese/English equivalents where relevant, for example ["bug", "缺陷", "错误", "fix", "修复"].
|
|
211
|
+
5. If a recall returns no results, use your judgment to make up to two additional recall calls before concluding the history has no answer. Each retry must use distinct entities chosen from semantic alternatives: abbreviations or expansions, aliases, translations, product or project names, and likely wording of the underlying task or decision. For example, after no result for "SAP BTP", try alternatives such as "BTP", "Business Technology Platform", and the specific platform/topic implied by the user's question.
|
|
212
|
+
6. Preserve every source, project, and time filter from the original request on retries. Do not repeat equivalent entities, search indefinitely, claim a result that was not returned, or say history was searched exhaustively after fewer than three total attempts.
|
|
211
213
|
|
|
212
214
|
Memory-aware response policy:
|
|
213
215
|
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.
|
|
215
|
-
3. After explaining a meaningful
|
|
216
|
-
4. When an existing durable memory resolves the question and has no
|
|
216
|
+
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.
|
|
217
|
+
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.
|
|
218
|
+
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
219
|
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
220
|
|
|
219
221
|
Session-expansion policy:
|
|
@@ -223,21 +225,20 @@ Session-expansion policy:
|
|
|
223
225
|
|
|
224
226
|
Pagination policy:
|
|
225
227
|
1. Each invocation returns five results. Local retrieval still evaluates every match before selecting that page.
|
|
226
|
-
2. When the result reports a \`nextOffset\`, call \`recall_memory\` again with the exact same
|
|
228
|
+
2. When the result reports a \`nextOffset\`, call \`recall_memory\` again with the exact same entities and scope filters plus that offset only when more candidates are needed. Do not request pages merely to exhaust the result set.
|
|
227
229
|
|
|
228
|
-
Extract 2–
|
|
230
|
+
Extract 2–8 specific, high-signal entities from the user's topic: project names, tool names, technologies, domain terms, identifiers, and useful Chinese/English equivalents, aliases, or abbreviations.`,
|
|
229
231
|
promptSnippet: "Search cross-client Pi, Claude Code, and Codex history when the user asks about prior discussions or work.",
|
|
230
232
|
|
|
231
233
|
parameters: Type.Object({
|
|
232
|
-
|
|
233
|
-
entities: Type.Optional(Type.Array(
|
|
234
|
+
entities: Type.Array(
|
|
234
235
|
Type.String({ minLength: 1 }),
|
|
235
236
|
{
|
|
236
|
-
description: '
|
|
237
|
-
minItems:
|
|
237
|
+
description: 'Two to eight high-signal literal search alternatives. Results may match any entity; include useful Chinese/English equivalents, aliases, or abbreviations. E.g. ["pi-session-memory", "bug", "缺陷", "错误", "fix", "修复"].',
|
|
238
|
+
minItems: 2,
|
|
238
239
|
maxItems: 8,
|
|
239
240
|
},
|
|
240
|
-
)
|
|
241
|
+
),
|
|
241
242
|
sources: Type.Optional(Type.Array(Type.Union([
|
|
242
243
|
Type.Literal("pi"), Type.Literal("claude"), Type.Literal("codex"),
|
|
243
244
|
]))),
|
|
@@ -248,13 +249,13 @@ Extract 2–5 specific entities from the user's topic: project names, tool names
|
|
|
248
249
|
}),
|
|
249
250
|
|
|
250
251
|
/** Resolve an agent memory request into one explicit page of a fully evaluated local result set. */
|
|
251
|
-
async execute(_toolCallId, {
|
|
252
|
-
const results = recallMemories({
|
|
252
|
+
async execute(_toolCallId, { entities, sources, cwd, after, before, offset }) {
|
|
253
|
+
const results = recallMemories({ entities, sources, cwd, after, before });
|
|
253
254
|
const page = paginateRecallResults(results, offset);
|
|
254
|
-
const text = formatRecallResults(page.results, {
|
|
255
|
+
const text = formatRecallResults(page.results, { entities, sources, cwd, after, before }, page);
|
|
255
256
|
return {
|
|
256
257
|
content: [{ type: "text" as const, text }],
|
|
257
|
-
details: {
|
|
258
|
+
details: { entities, sources, cwd, after, before, offset: page.offset, pageSize: page.results.length, totalResults: page.totalResults, nextOffset: page.nextOffset },
|
|
258
259
|
};
|
|
259
260
|
},
|
|
260
261
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-session-memory",
|
|
3
|
-
"version": "0.
|
|
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.1",
|
|
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
|
@@ -4,8 +4,8 @@ import { getDb, type MemoryKind } from "./db.ts";
|
|
|
4
4
|
export type MemorySource = "pi" | "claude" | "codex";
|
|
5
5
|
|
|
6
6
|
export interface RecallOptions {
|
|
7
|
-
|
|
8
|
-
entities
|
|
7
|
+
/** High-signal literal alternatives; a result may match any entity. */
|
|
8
|
+
entities: string[];
|
|
9
9
|
sources?: MemorySource[];
|
|
10
10
|
cwd?: string;
|
|
11
11
|
after?: number;
|
|
@@ -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;
|
|
@@ -60,20 +72,22 @@ const RECENCY_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
|
|
|
60
72
|
|
|
61
73
|
/** Retained for compatibility with the v0.1 public retrieval helper. */
|
|
62
74
|
export function recallTurns(entities: string[]): RecallTurnResult[] {
|
|
63
|
-
return _recallTurns({
|
|
75
|
+
return _recallTurns({ entities });
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/** Retrieve active durable memories, then raw turns not already represented by unchanged source evidence. */
|
|
67
79
|
export function recallMemories(options: RecallOptions): RecallResult[] {
|
|
68
80
|
const durableMemories = _recallDurableMemories(options);
|
|
69
81
|
const recalledTurns = _recallTurns(options);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
@@ -91,8 +105,8 @@ export function paginateRecallResults(results: RecallResult[], offset = 0): Reca
|
|
|
91
105
|
return { results: pageResults, offset, totalResults: results.length, nextOffset };
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
/** Render the exact
|
|
95
|
-
export function formatRecallResults(results: RecallResult[], options?: Pick<RecallOptions, "
|
|
108
|
+
/** Render the exact entity inputs and recall results as concise Markdown for a command notification or tool response. */
|
|
109
|
+
export function formatRecallResults(results: RecallResult[], options?: Pick<RecallOptions, "entities" | "sources" | "cwd" | "after" | "before">, page?: Omit<RecallPage, "results">): string {
|
|
96
110
|
const lines = options ? [_formatRecallQuery(options), ""] : [];
|
|
97
111
|
if (results.length === 0) return [...lines, page && page.totalResults > 0 ? `No results at offset ${page.offset}; the matching result set contains ${page.totalResults} result(s).` : "No relevant past conversations found."].join("\n");
|
|
98
112
|
|
|
@@ -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.
|
|
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}]`);
|
|
@@ -121,16 +142,15 @@ export function formatRecallResults(results: RecallResult[], options?: Pick<Reca
|
|
|
121
142
|
return lines.join("\n");
|
|
122
143
|
}
|
|
123
144
|
|
|
124
|
-
/** Make each tool invocation auditable by showing its
|
|
125
|
-
function _formatRecallQuery(options: Pick<RecallOptions, "
|
|
126
|
-
const
|
|
127
|
-
options.entities?.length ? `entities: ${options.entities.map((entity) => `\`${entity}\``).join(", ")}` : null,
|
|
145
|
+
/** Make each tool invocation auditable by showing its literal entities separately from its scope. */
|
|
146
|
+
function _formatRecallQuery(options: Pick<RecallOptions, "entities" | "sources" | "cwd" | "after" | "before">): string {
|
|
147
|
+
const scope = [
|
|
128
148
|
options.sources?.length ? `sources: ${options.sources.join(", ")}` : null,
|
|
129
149
|
options.cwd ? `cwd: \`${options.cwd}\`` : null,
|
|
130
150
|
options.after !== undefined ? `after: ${new Date(options.after).toISOString()}` : null,
|
|
131
151
|
options.before !== undefined ? `before: ${new Date(options.before).toISOString()}` : null,
|
|
132
152
|
].filter(Boolean);
|
|
133
|
-
return `**Search
|
|
153
|
+
return `**Search entities:** ${options.entities.map((entity) => `\`${entity}\``).join(", ")}${scope.length ? ` \\n**Scope:** ${scope.join(" · ")}` : ""}`;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
/** Keep discovery results small; full persisted turn text belongs to fetch_session. */
|
|
@@ -145,7 +165,7 @@ function _recallDurableMemories(options: RecallOptions): RecallDurableMemoryResu
|
|
|
145
165
|
if (terms.length === 0) return [];
|
|
146
166
|
const scoreExpression = terms.map(() => "CASE WHEN LOWER(content) LIKE ? ESCAPE '\\' THEN 1 ELSE 0 END").join(" + ");
|
|
147
167
|
const parameters = terms.map(_likePattern);
|
|
148
|
-
const filters = terms.map(() => "LOWER(content) LIKE ? ESCAPE '\\'");
|
|
168
|
+
const filters = [`(${terms.map(() => "LOWER(content) LIKE ? ESCAPE '\\'").join(" OR ")})`];
|
|
149
169
|
const filterParameters: Array<string | number> = terms.map(_likePattern);
|
|
150
170
|
filters.push("superseded_by IS NULL");
|
|
151
171
|
if (options.cwd) {
|
|
@@ -167,7 +187,45 @@ function _recallDurableMemories(options: RecallOptions): RecallDurableMemoryResu
|
|
|
167
187
|
WHERE ${filters.join(" AND ")}
|
|
168
188
|
ORDER BY hits DESC, importance DESC, last_confirmed_at DESC
|
|
169
189
|
`).all(...parameters, ...filterParameters)
|
|
170
|
-
.map((memory) => ({ ...memory, type: "memory" as const, freshness_candidate: false, score: memory.hits + memory.importance })) as RecallDurableMemoryResult[];
|
|
190
|
+
.map((memory) => ({ ...memory, type: "memory" as const, source_session_changed: false, freshness_candidate: false, freshness_evidence: [], score: memory.hits + memory.importance })) as RecallDurableMemoryResult[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Detect later activity in the original session independently of this recall query and ranking. */
|
|
194
|
+
function _sourceSessionChanged(memory: RecallDurableMemoryResult): boolean {
|
|
195
|
+
if (!memory.source_session_id || memory.source_turn_index === null) return false;
|
|
196
|
+
return getDb().prepare(`
|
|
197
|
+
SELECT 1 FROM turns
|
|
198
|
+
WHERE session_id = ? AND turn_index > ?
|
|
199
|
+
LIMIT 1
|
|
200
|
+
`).get(memory.source_session_id, memory.source_turn_index) !== undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Associate each memory with all newer query-relevant turns without deciding their semantic relationship. */
|
|
204
|
+
function _freshnessEvidence(memory: RecallDurableMemoryResult, recalledTurns: RecallTurnResult[]): FreshnessEvidence[] {
|
|
205
|
+
const baselineTs = _memoryEvidenceTimestamp(memory);
|
|
206
|
+
return recalledTurns
|
|
207
|
+
.filter((turn) => {
|
|
208
|
+
if (memory.source_session_id === turn.session_id && memory.source_turn_index !== null) {
|
|
209
|
+
return turn.turn_index > memory.source_turn_index;
|
|
210
|
+
}
|
|
211
|
+
return turn.ts > baselineTs;
|
|
212
|
+
})
|
|
213
|
+
.map((turn) => ({
|
|
214
|
+
turn_id: turn.turn_id,
|
|
215
|
+
session_id: turn.session_id,
|
|
216
|
+
turn_index: turn.turn_index,
|
|
217
|
+
source: turn.source,
|
|
218
|
+
ts: turn.ts,
|
|
219
|
+
relation: memory.source_session_id === turn.session_id ? "same_source_session_later" as const : "newer_cross_session" as const,
|
|
220
|
+
excerpt: _excerpt(turn.user_text || turn.reply_text),
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Use source-turn time when available; explicit memories become comparable from their creation time. */
|
|
225
|
+
function _memoryEvidenceTimestamp(memory: RecallDurableMemoryResult): number {
|
|
226
|
+
if (!memory.source_turn_id) return memory.created_at;
|
|
227
|
+
const source = getDb().prepare("SELECT ts FROM turns WHERE turn_id = ?").get(memory.source_turn_id) as { ts: number } | undefined;
|
|
228
|
+
return source?.ts ?? memory.created_at;
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
/** Retrieve and rank locally stored turns using literal query terms and optional scopes. */
|
|
@@ -178,7 +236,7 @@ function _recallTurns(options: RecallOptions): RecallTurnResult[] {
|
|
|
178
236
|
const scoreParameters = terms.flatMap((term) => _likeParameters(term));
|
|
179
237
|
const whereExpressions = terms.map(() => "(LOWER(turns.user_text) LIKE ? ESCAPE '\\' OR LOWER(turns.reply_text) LIKE ? ESCAPE '\\')");
|
|
180
238
|
const whereParameters = terms.flatMap((term) => _likeParameters(term));
|
|
181
|
-
const filters = [
|
|
239
|
+
const filters = [`(${whereExpressions.join(" OR ")})`];
|
|
182
240
|
const filterParameters: Array<string | number> = [...whereParameters];
|
|
183
241
|
if (options.sources?.length) {
|
|
184
242
|
filters.push(`sessions.source IN (${options.sources.map(() => "?").join(", ")})`);
|
|
@@ -213,9 +271,9 @@ function _turnContentHash(turn: RecallTurnResult): string {
|
|
|
213
271
|
return createHash("sha256").update(JSON.stringify([turn.user_text, turn.reply_text])).digest("hex");
|
|
214
272
|
}
|
|
215
273
|
|
|
216
|
-
/** Build a de-duplicated set of non-empty literal search
|
|
274
|
+
/** Build a de-duplicated set of non-empty literal search entities from the request. */
|
|
217
275
|
function _terms(options: RecallOptions): string[] {
|
|
218
|
-
return [...new Set(
|
|
276
|
+
return [...new Set(options.entities.map((entity) => entity.trim()).filter(Boolean))];
|
|
219
277
|
}
|
|
220
278
|
|
|
221
279
|
/** Produce matching user and assistant SQL LIKE parameters for one term. */
|