pi-session-memory 0.1.4 → 0.2.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 +101 -12
- package/extensions/index.ts +254 -19
- package/package.json +6 -2
- package/src/backfill.ts +102 -24
- package/src/db.ts +254 -4
- package/src/helper.ts +44 -0
- package/src/retriever.ts +213 -48
- package/src/session-migration.ts +188 -0
- package/src/writer.ts +3 -0
- package/spec.md +0 -172
- package/tests/core.test.ts +0 -68
- package/tsconfig.json +0 -10
package/README.md
CHANGED
|
@@ -9,22 +9,44 @@ A local-first Pi extension that saves completed conversations to SQLite and give
|
|
|
9
9
|
|
|
10
10
|
- Persists completed Pi conversations in `~/.pi/agent/memory.db`.
|
|
11
11
|
- Automatically imports only new or changed Pi, Claude Code, and Codex session files when Pi starts.
|
|
12
|
-
- Supports a forced full rescan with `/memory-backfill`.
|
|
12
|
+
- Supports a forced full SQLite history rescan with `/memory-backfill`.
|
|
13
|
+
- Converts current-project Codex sessions into separate native Pi sessions with `/project-session-migration`, ready to select through `/resume`.
|
|
13
14
|
- Exposes `recall_memory`, allowing Pi to retrieve relevant prior discussions when users explicitly refer to earlier work.
|
|
14
15
|
- Uses stable native user-message IDs and `INSERT OR IGNORE`, making live persistence and backfill idempotent.
|
|
15
16
|
- Searches literal substrings with escaped SQLite `LIKE` patterns, including technical terms containing `%`, `_`, or `\\`.
|
|
17
|
+
- Ranks results by literal relevance and recency, boosts an explicitly scoped project, and limits results to two turns per session for diversity.
|
|
18
|
+
- Supports explicit durable memories, which remain after their source transcript turns are deleted.
|
|
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.
|
|
21
|
+
- Includes status, direct search, and permanent deletion commands so users can inspect and control local memory.
|
|
16
22
|
- Uses only Node.js built-ins and SQLite (`node:sqlite`); no external runtime dependencies.
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
20
26
|
```bash
|
|
21
|
-
pi install npm:pi-session-memory
|
|
27
|
+
pi install npm:pi-session-memory
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
This unpinned source can receive package-update checks at Pi startup. After a new release, update it explicitly with:
|
|
25
31
|
|
|
26
32
|
```bash
|
|
27
|
-
pi
|
|
33
|
+
pi update npm:pi-session-memory
|
|
34
|
+
# or update every unpinned Pi extension
|
|
35
|
+
pi update --extensions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Restart Pi after the update to load the new extension code.
|
|
39
|
+
|
|
40
|
+
To try the latest package without installing it permanently:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pi -e npm:pi-session-memory
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To intentionally pin a known version (which `pi update --extensions` skips), add its version explicitly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pi install npm:pi-session-memory@0.2.1
|
|
28
50
|
```
|
|
29
51
|
|
|
30
52
|
## Usage
|
|
@@ -50,6 +72,32 @@ The command imports eligible user/assistant exchanges from:
|
|
|
50
72
|
| Claude Code | `~/.claude/projects/**/*.jsonl` |
|
|
51
73
|
| Codex | `~/.codex/sessions/**/*.jsonl` |
|
|
52
74
|
|
|
75
|
+
### Continue a Codex session natively in Pi
|
|
76
|
+
|
|
77
|
+
Use this command only when you want to continue prior **Codex** work as a real Pi session rather than search it as memory:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/project-session-migration
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It selects Codex JSONL files whose recorded `cwd` exactly equals the current project, then creates one independent Pi v3 session JSONL for each under Pi's normal session directory. Each migrated entry is named `Migrated from Codex: <session-id>`.
|
|
84
|
+
|
|
85
|
+
After the command completes, run:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
/resume
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
and select the migrated session to continue it in Pi. Existing migrated outputs are skipped on subsequent runs.
|
|
92
|
+
|
|
93
|
+
The converter preserves user and assistant text messages only. It deliberately does not convert Codex system/developer prompts, reasoning, tool calls, or tool results into Pi messages. Migration is separate from SQLite backfill and `recall_memory`; normal cross-client recall does not require migration.
|
|
94
|
+
|
|
95
|
+
#### Context compaction and retention
|
|
96
|
+
|
|
97
|
+
Compaction reduces what a running client sends to its model; it is not necessarily deletion of the local JSONL history. In locally observed Claude Code and Codex files, compaction is appended as a separate event (`system/compact_boundary` for Claude Code and `compacted` for Codex), while earlier user/assistant message records remain in the file and remain importable. This is observed behavior rather than a retention guarantee from those clients.
|
|
98
|
+
|
|
99
|
+
The extension indexes normalized user/assistant text and selected tool names, not every JSONL event. It intentionally excludes reasoning, full tool inputs/outputs, system/developer context, workspace state, Pi custom records, Claude compaction metadata, and Codex handoff summaries. See [`quick-notes/jsonl-schema.md`](quick-notes/jsonl-schema.md) for source-specific formats and the exact importer boundary.
|
|
100
|
+
|
|
53
101
|
### Recall prior work
|
|
54
102
|
|
|
55
103
|
The extension instructs Pi to call `recall_memory` when a user explicitly asks about a previous discussion, for example:
|
|
@@ -58,21 +106,50 @@ The extension instructs Pi to call `recall_memory` when a user explicitly asks a
|
|
|
58
106
|
What did we decide about LangGraph last time?
|
|
59
107
|
```
|
|
60
108
|
|
|
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
|
+
|
|
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.
|
|
112
|
+
|
|
113
|
+
### Inspect and control memory
|
|
114
|
+
|
|
115
|
+
Recall and freshness explanations are automatic model behavior. The commands below remain deliberate user-control actions for inspecting or changing stored memory; they are not required for normal recalled answers.
|
|
116
|
+
|
|
61
117
|
```text
|
|
62
|
-
|
|
118
|
+
/remember Keep SQLite writes local only
|
|
119
|
+
/memory-pin pi:<session-id>:<user-message-id>
|
|
120
|
+
/memory-list [decision]
|
|
121
|
+
/memory-confirm <memory-id>
|
|
122
|
+
/memory-supersede <old-memory-id> <new-memory-id>
|
|
123
|
+
/memory-history <memory-id>
|
|
124
|
+
/memory-forget <memory-id>
|
|
125
|
+
/memory-delete-turn pi:<session-id>:<user-message-id>
|
|
126
|
+
/memory-status
|
|
127
|
+
/memory-search SQLite migration
|
|
63
128
|
```
|
|
64
129
|
|
|
65
|
-
|
|
130
|
+
`fetch_session` is an agent tool, not a user command: Pi invokes it selectively after `recall_memory` when it needs additional context from a specific discovered session.
|
|
131
|
+
|
|
132
|
+
- `/remember <text>` saves an explicit durable `fact` scoped to the current project.
|
|
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
|
+
- `/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.
|
|
136
|
+
- `/memory-confirm <memory-id>` records that an active memory remains current by updating `last_confirmed_at`.
|
|
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
|
+
- `/memory-history <memory-id>` displays the complete oldest-to-newest supersession chain.
|
|
139
|
+
- `/memory-forget <memory-id>` permanently deletes a durable memory without deleting its source transcript.
|
|
140
|
+
- `/memory-delete-turn <turn-id>` permanently deletes one raw turn; its session is also removed if it has no turns left.
|
|
141
|
+
- `/memory-status` reports turn and session totals, per-source distribution, and newest memory time.
|
|
142
|
+
- `/memory-search <query>` previews the same local literal retrieval used by the agent.
|
|
66
143
|
|
|
67
144
|
## How it works
|
|
68
145
|
|
|
69
146
|
```text
|
|
70
|
-
Pi
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
147
|
+
Pi / Claude Code / Codex history ──► incremental source sync ──► SQLite memory.db
|
|
148
|
+
▲
|
|
149
|
+
│
|
|
150
|
+
recall_memory
|
|
151
|
+
|
|
152
|
+
Current-project Codex JSONL ──► /project-session-migration ──► native Pi session JSONL ──► /resume
|
|
76
153
|
```
|
|
77
154
|
|
|
78
155
|
After a Pi agent run settles, the extension captures the latest user message and the subsequent assistant replies/tool names from the active session branch. Historical imports normalize each supported source into the same session/turn schema.
|
|
@@ -87,6 +164,18 @@ The database stays on the local machine at:
|
|
|
87
164
|
|
|
88
165
|
Conversation data is stored and queried locally. This package does not add a remote storage service or transmit conversation history on its own. Review the source and your model provider's configuration before using it with sensitive conversations.
|
|
89
166
|
|
|
167
|
+
## Release notes
|
|
168
|
+
|
|
169
|
+
| Version | Highlights |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `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
|
+
| `0.2.0` | Added cross-client SQLite recall and durable-memory controls, plus native current-project Codex-to-Pi session migration for `/resume`. |
|
|
173
|
+
| `0.1.4` | Automatically syncs new or changed Pi, Claude Code, and Codex history when Pi starts; `/memory-backfill` forces a full rescan. |
|
|
174
|
+
| `0.1.3` | Improved package documentation and installation guidance. |
|
|
175
|
+
| `0.1.2` | Added the MIT license. |
|
|
176
|
+
| `0.1.1` | Added repository and package metadata for public distribution. |
|
|
177
|
+
| `0.1.0` | Initial release: local SQLite memory, Pi live persistence, historical import, and `recall_memory` retrieval. |
|
|
178
|
+
|
|
90
179
|
## Development
|
|
91
180
|
|
|
92
181
|
```bash
|
package/extensions/index.ts
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { writeTurn } from "../src/writer.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { confirmMemory, createMemory, deleteMemory, deleteTurn, getMemoryHistory, getMemoryStats, getSession, listMemories, pinTurnAsMemory, supersedeMemory, type MemoryKind } from "../src/db.ts";
|
|
5
|
+
import { recallMemories, formatRecallResults, paginateRecallResults } from "../src/retriever.ts";
|
|
6
|
+
import { backfillAll, syncChangedHistory, type BackfillStats } from "../src/backfill.ts";
|
|
7
|
+
import { migrateCodexProjectSessions, type ProjectSessionMigrationStats } from "../src/session-migration.ts";
|
|
8
|
+
import { SESSION_MEMORY_HELP } from "../src/helper.ts";
|
|
6
9
|
|
|
10
|
+
/** Register lifecycle persistence, memory-management commands, and the recall tool with Pi. */
|
|
7
11
|
export default function (pi: ExtensionAPI) {
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
/** Synchronize changed external session history whenever a Pi session starts. */
|
|
14
|
+
pi.on("session_start", async (event, ctx) => {
|
|
10
15
|
const stats = syncChangedHistory();
|
|
11
|
-
if (stats.scannedFiles > 0) {
|
|
16
|
+
if (event.reason === "startup" || stats.scannedFiles > 0) {
|
|
17
|
+
const summary = stats.scannedFiles > 0
|
|
18
|
+
? `synced ${stats.turns} turns from ${stats.scannedFiles} changed session files`
|
|
19
|
+
: "ready";
|
|
12
20
|
ctx.ui.notify(
|
|
13
|
-
`[session-memory]
|
|
21
|
+
`[session-memory] ${summary}. Run /pi-session-memory-helper for an overview of cross-client memory features.`,
|
|
14
22
|
"info",
|
|
15
23
|
);
|
|
16
24
|
}
|
|
25
|
+
for (const issue of stats.issues) ctx.ui.notify(`[session-memory] ${issue.error}`, "error");
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
// ── Write: persist each completed agent run to SQLite ────────────────────
|
|
29
|
+
/** Persist the just-completed live Pi turn after the agent has settled. */
|
|
20
30
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
21
31
|
try {
|
|
22
32
|
writeTurn(ctx);
|
|
@@ -25,14 +35,160 @@ export default function (pi: ExtensionAPI) {
|
|
|
25
35
|
}
|
|
26
36
|
});
|
|
27
37
|
|
|
38
|
+
pi.registerCommand("pi-session-memory-helper", {
|
|
39
|
+
description: "Show pi-session-memory features and commands",
|
|
40
|
+
/** Display the static pi-session-memory feature overview without invoking the model. */
|
|
41
|
+
handler: async (_args, ctx) => {
|
|
42
|
+
ctx.ui.notify(SESSION_MEMORY_HELP, "info");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
pi.registerCommand("memory-status", {
|
|
47
|
+
description: "Show local memory storage and source statistics",
|
|
48
|
+
/** Report aggregate local-memory storage statistics. */
|
|
49
|
+
handler: async (_args, ctx) => {
|
|
50
|
+
const stats = getMemoryStats();
|
|
51
|
+
const sources = stats.sources.length
|
|
52
|
+
? stats.sources.map((source) => `${source.source}: ${source.turns} turns / ${source.sessions} sessions`).join(", ")
|
|
53
|
+
: "no imported sources";
|
|
54
|
+
const newest = stats.newestTs ? new Date(stats.newestTs).toLocaleString() : "n/a";
|
|
55
|
+
ctx.ui.notify(`[session-memory] ${stats.turns} turns across ${stats.sessions} sessions; ${sources}; newest: ${newest}`, "info");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
pi.registerCommand("memory-search", {
|
|
60
|
+
description: "Search local memory with a literal query and show its first five results",
|
|
61
|
+
/** Search stored memory from a literal command query without rendering the entire match set. */
|
|
62
|
+
handler: async (args, ctx) => {
|
|
63
|
+
const query = args.trim();
|
|
64
|
+
if (!query) throw new Error("Usage: /memory-search <query>");
|
|
65
|
+
const page = paginateRecallResults(recallMemories({ query }));
|
|
66
|
+
ctx.ui.notify(formatRecallResults(page.results, { query }, page), "info");
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.registerCommand("remember", {
|
|
71
|
+
description: "Create an explicit durable fact memory in the current project",
|
|
72
|
+
/** Store user-authored reusable knowledge independently from transcript retention. */
|
|
73
|
+
handler: async (args, ctx) => {
|
|
74
|
+
const content = args.trim();
|
|
75
|
+
if (!content) throw new Error("Usage: /remember <text>");
|
|
76
|
+
const memory = createMemory({ kind: "fact", content, project_key: ctx.sessionManager.getCwd(), source_turn_id: null, importance: 1 });
|
|
77
|
+
ctx.ui.notify(`[session-memory] remembered ${memory.memory_id}`, "info");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pi.registerCommand("memory-pin", {
|
|
82
|
+
description: "Promote a historical turn into a durable fact memory",
|
|
83
|
+
/** Preserve a selected transcript turn as an independent durable memory with provenance. */
|
|
84
|
+
handler: async (args, ctx) => {
|
|
85
|
+
const turnId = args.trim();
|
|
86
|
+
if (!turnId) throw new Error("Usage: /memory-pin <turn-id>");
|
|
87
|
+
const memory = pinTurnAsMemory(turnId);
|
|
88
|
+
ctx.ui.notify(`[session-memory] pinned ${turnId} as ${memory.memory_id}`, "info");
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.registerCommand("memory-list", {
|
|
93
|
+
description: "List durable memories, optionally filtered by kind",
|
|
94
|
+
/** Present durable memories and their provenance for direct management. */
|
|
95
|
+
handler: async (args, ctx) => {
|
|
96
|
+
const kind = args.trim() as MemoryKind | "";
|
|
97
|
+
if (kind && !["preference", "decision", "fact", "project_state", "task", "lesson"].includes(kind)) {
|
|
98
|
+
throw new Error("Usage: /memory-list [preference|decision|fact|project_state|task|lesson]");
|
|
99
|
+
}
|
|
100
|
+
const memories = listMemories(kind || undefined);
|
|
101
|
+
const text = memories.length
|
|
102
|
+
? memories.map((memory) => `[${memory.kind}] ${memory.memory_id}: ${memory.content}${memory.source_turn_id ? ` (source: ${memory.source_turn_id})` : ""}`).join("\n")
|
|
103
|
+
: "No durable memories found.";
|
|
104
|
+
ctx.ui.notify(text, "info");
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pi.registerCommand("memory-confirm", {
|
|
109
|
+
description: "Confirm an active durable memory remains current",
|
|
110
|
+
/** Explicitly acknowledge current evidence without rewriting the memory or its provenance. */
|
|
111
|
+
handler: async (args, ctx) => {
|
|
112
|
+
const memoryId = args.trim();
|
|
113
|
+
if (!memoryId) throw new Error("Usage: /memory-confirm <memory-id>");
|
|
114
|
+
const memory = confirmMemory(memoryId);
|
|
115
|
+
ctx.ui.notify(`[session-memory] confirmed ${memory.memory_id}`, "info");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.registerCommand("memory-supersede", {
|
|
120
|
+
description: "Replace one durable memory with another while retaining history",
|
|
121
|
+
/** Make supersession explicit so obsolete decisions remain inspectable instead of being silently overwritten. */
|
|
122
|
+
handler: async (args, ctx) => {
|
|
123
|
+
const [oldMemoryId, newMemoryId, ...extra] = args.trim().split(/\s+/);
|
|
124
|
+
if (!oldMemoryId || !newMemoryId || extra.length) throw new Error("Usage: /memory-supersede <old-memory-id> <new-memory-id>");
|
|
125
|
+
supersedeMemory(oldMemoryId, newMemoryId);
|
|
126
|
+
ctx.ui.notify(`[session-memory] superseded ${oldMemoryId} with ${newMemoryId}`, "info");
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
pi.registerCommand("memory-history", {
|
|
131
|
+
description: "Show the replacement chain containing a durable memory",
|
|
132
|
+
/** Render the full predecessor-to-successor chain for an active or superseded memory. */
|
|
133
|
+
handler: async (args, ctx) => {
|
|
134
|
+
const memoryId = args.trim();
|
|
135
|
+
if (!memoryId) throw new Error("Usage: /memory-history <memory-id>");
|
|
136
|
+
const history = getMemoryHistory(memoryId);
|
|
137
|
+
ctx.ui.notify(history.map((memory) => `[${memory.superseded_by ? "superseded" : "active"}] ${memory.memory_id}: ${memory.content}`).join("\n"), "info");
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.registerCommand("memory-forget", {
|
|
142
|
+
description: "Permanently delete a durable memory by memory ID",
|
|
143
|
+
/** Delete only the selected durable memory; its provenance turn is retained. */
|
|
144
|
+
handler: async (args, ctx) => {
|
|
145
|
+
const memoryId = args.trim();
|
|
146
|
+
if (!memoryId) throw new Error("Usage: /memory-forget <memory-id>");
|
|
147
|
+
if (!deleteMemory(memoryId)) throw new Error(`Durable memory not found: ${memoryId}`);
|
|
148
|
+
ctx.ui.notify(`[session-memory] forgot ${memoryId}`, "info");
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
pi.registerCommand("memory-delete-turn", {
|
|
153
|
+
description: "Permanently delete a raw transcript turn by turn ID",
|
|
154
|
+
/** Keep raw transcript deletion explicit and separate from durable-memory deletion. */
|
|
155
|
+
handler: async (args, ctx) => {
|
|
156
|
+
const turnId = args.trim();
|
|
157
|
+
if (!turnId) throw new Error("Usage: /memory-delete-turn <turn-id>");
|
|
158
|
+
if (!deleteTurn(turnId)) throw new Error(`Memory turn not found: ${turnId}`);
|
|
159
|
+
ctx.ui.notify(`[session-memory] deleted transcript turn ${turnId}`, "info");
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
28
163
|
pi.registerCommand("memory-backfill", {
|
|
29
164
|
description: "Import historical Pi, Claude Code, and Codex sessions into memory.db",
|
|
165
|
+
/** Force a full historical import regardless of saved source-file fingerprints. */
|
|
30
166
|
handler: async (_args, ctx) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
167
|
+
_notifyBackfill(ctx, backfillAll(), "imported");
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
pi.registerCommand("project-session-migration", {
|
|
172
|
+
description: "Convert this project's Codex sessions into separate Pi sessions that can be resumed",
|
|
173
|
+
/** Create independently resumable Pi session JSONL files from current-project Codex sessions. */
|
|
174
|
+
handler: async (_args, ctx) => {
|
|
175
|
+
_notifySessionMigration(ctx, migrateCodexProjectSessions(ctx.sessionManager.getCwd()));
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
pi.registerTool({
|
|
180
|
+
name: "migrate_codex_project_sessions",
|
|
181
|
+
label: "Migrate Codex Project Sessions",
|
|
182
|
+
description: "Convert each Codex session for the current project into a separate native Pi session that the user can select with /resume. Call only when the user explicitly wants to continue prior Codex work as a resumable Pi session. This does not index history for recall_memory and must not be used for ordinary cross-client recall or durable-memory requests.",
|
|
183
|
+
promptSnippet: "Convert this project's Codex sessions into separately resumable Pi sessions only when the user explicitly requests native Pi continuation.",
|
|
184
|
+
parameters: Type.Object({}),
|
|
185
|
+
/** Give the agent the same native Codex-to-Pi migration available through /project-session-migration. */
|
|
186
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
187
|
+
const stats = migrateCodexProjectSessions(ctx.sessionManager.getCwd());
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text" as const, text: _sessionMigrationSummary(stats) }],
|
|
190
|
+
details: stats,
|
|
191
|
+
};
|
|
36
192
|
},
|
|
37
193
|
});
|
|
38
194
|
|
|
@@ -40,7 +196,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
196
|
pi.registerTool({
|
|
41
197
|
name: "recall_memory",
|
|
42
198
|
label: "Recall Memory",
|
|
43
|
-
description: `Search this user's past conversation history across Pi, Claude Code, and Codex.
|
|
199
|
+
description: `Search this user's past conversation history across Pi, Claude Code, and Codex. Use query for the user's full request and entities for its important identifiers. Filters only narrow the local search.
|
|
44
200
|
|
|
45
201
|
Invocation policy:
|
|
46
202
|
1. Call this tool immediately when the user explicitly asks to review, remember, summarize, continue, or compare a previous discussion about a topic. Examples:
|
|
@@ -50,27 +206,106 @@ Invocation policy:
|
|
|
50
206
|
- "之前那个方案怎么说的" / "what was that plan we had?"
|
|
51
207
|
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.
|
|
52
208
|
3. Do not search history merely because a question is difficult when the user's prior discussions are not relevant.
|
|
209
|
+
4. 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 a distinct, concise literal query and/or 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.
|
|
210
|
+
5. Preserve every source, project, and time filter from the original request on retries. Do not repeat an equivalent query, search indefinitely, claim a result that was not returned, or say history was searched exhaustively after fewer than three total attempts.
|
|
211
|
+
|
|
212
|
+
Memory-aware response policy:
|
|
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.
|
|
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
|
+
|
|
219
|
+
Session-expansion policy:
|
|
220
|
+
1. \`recall_memory\` is a discovery tool. Raw-turn results are intentionally short excerpts with session ID and turn index.
|
|
221
|
+
2. Call \`fetch_session\` only when a candidate's surrounding conversation is necessary to answer accurately, verify a conclusion, resolve a conflict, or inspect context around a matched turn. Use its turn bounds to request the smallest useful range.
|
|
222
|
+
3. Do not fetch a session when a durable memory or returned excerpt already answers the question. Do not fetch unrelated sessions merely because they were listed.
|
|
223
|
+
|
|
224
|
+
Pagination policy:
|
|
225
|
+
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 query and filters plus that offset only when more candidates are needed. Do not request pages merely to exhaust the result set.
|
|
53
227
|
|
|
54
|
-
Extract 2–5 specific entities from the user's topic: project names, tool names, technologies, domain terms, or identifiers.`,
|
|
228
|
+
Extract 2–5 specific entities from the user's topic: project names, tool names, technologies, domain terms, or identifiers.`,
|
|
229
|
+
promptSnippet: "Search cross-client Pi, Claude Code, and Codex history when the user asks about prior discussions or work.",
|
|
55
230
|
|
|
56
231
|
parameters: Type.Object({
|
|
57
|
-
|
|
232
|
+
query: Type.String({ minLength: 1, description: "The user's full memory request or its most specific literal phrase." }),
|
|
233
|
+
entities: Type.Optional(Type.Array(
|
|
58
234
|
Type.String({ minLength: 1 }),
|
|
59
235
|
{
|
|
60
|
-
description: '
|
|
236
|
+
description: 'Important identifiers from the request. E.g. ["payroll", "LangGraph", "A2A"]',
|
|
61
237
|
minItems: 1,
|
|
62
238
|
maxItems: 8,
|
|
63
239
|
},
|
|
64
|
-
),
|
|
240
|
+
)),
|
|
241
|
+
sources: Type.Optional(Type.Array(Type.Union([
|
|
242
|
+
Type.Literal("pi"), Type.Literal("claude"), Type.Literal("codex"),
|
|
243
|
+
]))),
|
|
244
|
+
cwd: Type.Optional(Type.String({ minLength: 1, description: "Exact project working directory to restrict results." })),
|
|
245
|
+
after: Type.Optional(Type.Number({ description: "Inclusive Unix timestamp in milliseconds." })),
|
|
246
|
+
before: Type.Optional(Type.Number({ description: "Inclusive Unix timestamp in milliseconds." })),
|
|
247
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based result offset. Each call returns five results; use the returned nextOffset with identical search and filter inputs only when more candidates are needed." })),
|
|
65
248
|
}),
|
|
66
249
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
250
|
+
/** Resolve an agent memory request into one explicit page of a fully evaluated local result set. */
|
|
251
|
+
async execute(_toolCallId, { query, entities, sources, cwd, after, before, offset }) {
|
|
252
|
+
const results = recallMemories({ query, entities, sources, cwd, after, before });
|
|
253
|
+
const page = paginateRecallResults(results, offset);
|
|
254
|
+
const text = formatRecallResults(page.results, { query, entities, sources, cwd, after, before }, page);
|
|
70
255
|
return {
|
|
71
256
|
content: [{ type: "text" as const, text }],
|
|
72
|
-
details: { entities,
|
|
257
|
+
details: { query, entities, sources, cwd, after, before, offset: page.offset, pageSize: page.results.length, totalResults: page.totalResults, nextOffset: page.nextOffset },
|
|
73
258
|
};
|
|
74
259
|
},
|
|
75
260
|
});
|
|
261
|
+
|
|
262
|
+
pi.registerTool({
|
|
263
|
+
name: "fetch_session",
|
|
264
|
+
label: "Fetch Session",
|
|
265
|
+
description: "Fetch ordered persisted conversation turns for one session returned by recall_memory. Use only when the candidate excerpt or durable memory is insufficient and surrounding context is necessary. Request the smallest useful inclusive turn-index range.",
|
|
266
|
+
promptSnippet: "Expand only a recall_memory result when its excerpt is insufficient; request the smallest useful turn range.",
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
session_id: Type.String({ minLength: 1, description: "Exact session ID returned by recall_memory." }),
|
|
269
|
+
from_turn_index: Type.Optional(Type.Number({ minimum: 0, description: "Optional inclusive first turn index." })),
|
|
270
|
+
to_turn_index: Type.Optional(Type.Number({ minimum: 0, description: "Optional inclusive last turn index." })),
|
|
271
|
+
}),
|
|
272
|
+
/** Expand a specifically selected persisted session without coupling search ranking to context payload size. */
|
|
273
|
+
async execute(_toolCallId, { session_id, from_turn_index, to_turn_index }) {
|
|
274
|
+
const stored = getSession(session_id, from_turn_index, to_turn_index);
|
|
275
|
+
const header = `## Session ${stored.session.session_id}\n**Source:** ${stored.session.source} · **Project:** \`${stored.session.cwd}\`\n**Turns:** ${stored.turns.length}`;
|
|
276
|
+
const turns = stored.turns.length
|
|
277
|
+
? stored.turns.map((turn) => [
|
|
278
|
+
`### Turn ${turn.turn_index} · ${new Date(turn.ts).toLocaleString()}`,
|
|
279
|
+
`**You:** ${turn.user_text}`,
|
|
280
|
+
turn.reply_text ? `**Assistant:** ${turn.reply_text}` : "",
|
|
281
|
+
].filter(Boolean).join("\n")).join("\n\n")
|
|
282
|
+
: "No persisted turns in the requested range.";
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text" as const, text: `${header}\n\n${turns}` }],
|
|
285
|
+
details: { session_id, from_turn_index, to_turn_index, turnCount: stored.turns.length },
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Present one backfill result consistently for user commands and agent tool calls. */
|
|
292
|
+
function _backfillSummary(stats: BackfillStats, action: string): string {
|
|
293
|
+
return `[session-memory] ${action} ${stats.turns} turns from ${stats.scannedFiles} files: ${stats.pi} Pi, ${stats.claude} Claude, ${stats.codex} Codex sessions`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Notify command users of one backfill summary and every isolated import issue. */
|
|
297
|
+
function _notifyBackfill(ctx: ExtensionContext, stats: BackfillStats, action: string): void {
|
|
298
|
+
ctx.ui.notify(_backfillSummary(stats, action), "info");
|
|
299
|
+
for (const issue of stats.issues) ctx.ui.notify(`[session-memory] ${issue.error}`, "error");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Report native Codex-to-Pi session migration results and isolated conversion failures. */
|
|
303
|
+
function _notifySessionMigration(ctx: ExtensionContext, stats: ProjectSessionMigrationStats): void {
|
|
304
|
+
ctx.ui.notify(_sessionMigrationSummary(stats), "info");
|
|
305
|
+
for (const issue of stats.issues) ctx.ui.notify(`[session-memory] Codex migration failed (${issue.path}): ${issue.error}`, "error");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Format a concise native-session migration result for commands and tools. */
|
|
309
|
+
function _sessionMigrationSummary(stats: ProjectSessionMigrationStats): string {
|
|
310
|
+
return `[session-memory] migrated ${stats.migratedSessions} Codex sessions (${stats.migratedMessages} messages); skipped ${stats.skippedSessions} already migrated sessions from ${stats.scannedFiles} scanned files. Use /resume to select a migrated Pi session.`;
|
|
76
311
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-session-memory",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Persistent, local-first cross-session memory for Pi, with SQLite-backed recall across Pi, Claude Code, and Codex conversations",
|
|
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",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"url": "https://github.com/shengxiao20/pi-session-memory/issues"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
|
+
"files": [
|
|
19
|
+
"extensions/",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
18
22
|
"scripts": {
|
|
19
23
|
"test": "tsx tests/core.test.ts"
|
|
20
24
|
},
|