pi-session-memory 0.3.0 → 0.4.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 +23 -15
- package/extensions/index.ts +54 -29
- package/package.json +1 -1
- package/src/helper.ts +9 -9
- package/src/retriever.ts +13 -14
- package/src/session-migration.ts +126 -30
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ A local-first Pi extension that saves completed conversations to SQLite and give
|
|
|
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
12
|
- Supports a forced full SQLite history rescan with `/memory-backfill`.
|
|
13
|
-
- Converts current-project Codex sessions into separate native Pi sessions
|
|
13
|
+
- Converts current-project Codex or Claude Code sessions into separate native Pi sessions, ready to select through `/resume`.
|
|
14
14
|
- Exposes `recall_memory`, allowing Pi to retrieve relevant prior discussions when users explicitly refer to earlier work.
|
|
15
15
|
- Uses stable native user-message IDs and `INSERT OR IGNORE`, making live persistence and backfill idempotent.
|
|
16
16
|
- Searches literal substrings with escaped SQLite `LIKE` patterns, including technical terms containing `%`, `_`, or `\\`.
|
|
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
|
-
- Associates recalled durable memories with later source-session activity and newer
|
|
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.4.0
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Usage
|
|
@@ -72,15 +72,16 @@ The command imports eligible user/assistant exchanges from:
|
|
|
72
72
|
| Claude Code | `~/.claude/projects/**/*.jsonl` |
|
|
73
73
|
| Codex | `~/.codex/sessions/**/*.jsonl` |
|
|
74
74
|
|
|
75
|
-
### Continue a Codex session natively in Pi
|
|
75
|
+
### Continue a Claude Code or Codex session natively in Pi
|
|
76
76
|
|
|
77
|
-
Use
|
|
77
|
+
Use a migration command only when you want to continue prior work as a real Pi session rather than search it as memory:
|
|
78
78
|
|
|
79
79
|
```text
|
|
80
|
-
/project-session-migration
|
|
80
|
+
/project-claude-session-migration # Claude Code → Pi
|
|
81
|
+
/project-session-migration # Codex → Pi
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
Each command selects only source JSONL files whose recorded `cwd` exactly equals the current project, then creates one independent Pi v3 session JSONL under Pi's normal session directory. Entries are named `Migrated from Claude Code: <session-id>` or `Migrated from Codex: <session-id>`.
|
|
84
85
|
|
|
85
86
|
After the command completes, run:
|
|
86
87
|
|
|
@@ -90,7 +91,7 @@ After the command completes, run:
|
|
|
90
91
|
|
|
91
92
|
and select the migrated session to continue it in Pi. Existing migrated outputs are skipped on subsequent runs.
|
|
92
93
|
|
|
93
|
-
The
|
|
94
|
+
The converters preserve user and assistant text messages only. They deliberately do not convert client-injected context, 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
|
|
|
95
96
|
#### Context compaction and retention
|
|
96
97
|
|
|
@@ -106,9 +107,9 @@ The extension instructs Pi to call `recall_memory` when a user explicitly asks a
|
|
|
106
107
|
What did we decide about LangGraph last time?
|
|
107
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
|
|
110
|
+
`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
111
|
|
|
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
|
|
112
|
+
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
113
|
|
|
113
114
|
### Inspect and control memory
|
|
114
115
|
|
|
@@ -132,7 +133,7 @@ Recall and freshness explanations are automatic model behavior. The commands bel
|
|
|
132
133
|
- `/remember <text>` saves an explicit durable `fact` scoped to the current project.
|
|
133
134
|
- `/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
135
|
- `/memory-list [kind]` displays durable memories, optionally limited to `preference`, `decision`, `fact`, `project_state`, `task`, or `lesson`.
|
|
135
|
-
- Recall distinguishes later activity in a memory's source session from newer
|
|
136
|
+
- 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
137
|
- `/memory-confirm <memory-id>` records that an active memory remains current by updating `last_confirmed_at`.
|
|
137
138
|
- `/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
139
|
- `/memory-history <memory-id>` displays the complete oldest-to-newest supersession chain.
|
|
@@ -149,7 +150,9 @@ Pi / Claude Code / Codex history ──► incremental source sync ──► SQL
|
|
|
149
150
|
│
|
|
150
151
|
recall_memory
|
|
151
152
|
|
|
152
|
-
Current-project
|
|
153
|
+
Current-project Claude Code JSONL ──► /project-claude-session-migration ─┐
|
|
154
|
+
Current-project Codex JSONL ────────► /project-session-migration ───────┼──► native Pi session JSONL ──► /resume
|
|
155
|
+
┘
|
|
153
156
|
```
|
|
154
157
|
|
|
155
158
|
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.
|
|
@@ -168,7 +171,9 @@ Conversation data is stored and queried locally. This package does not add a rem
|
|
|
168
171
|
|
|
169
172
|
| Version | Highlights |
|
|
170
173
|
| --- | --- |
|
|
171
|
-
| `0.
|
|
174
|
+
| `0.4.0` | Adds native current-project Claude Code-to-Pi session migration through `/project-claude-session-migration` and `migrate_claude_project_sessions`. Claude Code and Codex now share deterministic, source-namespaced, idempotent Pi v3 session output; SQLite data and schema remain compatible, so no database migration is required. |
|
|
175
|
+
| `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. |
|
|
176
|
+
| `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. |
|
|
172
177
|
| `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. |
|
|
173
178
|
| `0.2.0` | Added cross-client SQLite recall and durable-memory controls, plus native current-project Codex-to-Pi session migration for `/resume`. |
|
|
174
179
|
| `0.1.4` | Automatically syncs new or changed Pi, Claude Code, and Codex history when Pi starts; `/memory-backfill` forces a full rescan. |
|
|
@@ -179,18 +184,21 @@ Conversation data is stored and queried locally. This package does not add a rem
|
|
|
179
184
|
|
|
180
185
|
## Release compatibility review
|
|
181
186
|
|
|
182
|
-
Before every significant release, review these compatibility surfaces and record any migration or versioning decision:
|
|
187
|
+
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:
|
|
183
188
|
|
|
184
189
|
1. **Install/package:** package name, Pi manifest, runtime dependencies, and published file allowlist.
|
|
185
190
|
2. **Persistent data:** SQLite schema/migrations, JSONL-import compatibility, and any data rewrite.
|
|
186
191
|
3. **Agent tools and commands:** tool names, input schemas, result/details contracts, and slash commands.
|
|
187
192
|
4. **Retrieval and agent behavior:** ranking, pagination, freshness/evidence semantics, prompt policy, and automatic side effects.
|
|
188
193
|
5. **Public TypeScript/module API:** exported types/functions and required result fields.
|
|
194
|
+
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.
|
|
189
195
|
|
|
190
|
-
The `0.
|
|
196
|
+
The `0.4.0` review found no SQLite breaking change: Claude Code native-session migration adds commands, an agent tool, and Pi session-file output only. 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 minor version because it adds a backward-compatible user capability.
|
|
191
197
|
|
|
192
198
|
## Development
|
|
193
199
|
|
|
194
200
|
```bash
|
|
195
201
|
npm test
|
|
196
202
|
```
|
|
203
|
+
|
|
204
|
+
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
|
@@ -4,7 +4,7 @@ import { writeTurn } from "../src/writer.ts";
|
|
|
4
4
|
import { confirmMemory, createMemory, deleteMemory, deleteTurn, getMemoryHistory, getMemoryStats, getSession, listMemories, pinTurnAsMemory, supersedeMemory, type MemoryKind } from "../src/db.ts";
|
|
5
5
|
import { recallMemories, formatRecallResults, paginateRecallResults } from "../src/retriever.ts";
|
|
6
6
|
import { backfillAll, syncChangedHistory, type BackfillStats } from "../src/backfill.ts";
|
|
7
|
-
import { migrateCodexProjectSessions, type ProjectSessionMigrationStats } from "../src/session-migration.ts";
|
|
7
|
+
import { migrateClaudeProjectSessions, migrateCodexProjectSessions, type ProjectSessionMigrationStats } from "../src/session-migration.ts";
|
|
8
8
|
import { SESSION_MEMORY_HELP } from "../src/helper.ts";
|
|
9
9
|
|
|
10
10
|
/** Register lifecycle persistence, memory-management commands, and the recall tool with Pi. */
|
|
@@ -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
|
|
|
@@ -169,24 +170,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
170
|
});
|
|
170
171
|
|
|
171
172
|
pi.registerCommand("project-session-migration", {
|
|
172
|
-
description: "Convert
|
|
173
|
+
description: "Convert current-project Codex sessions into separate native Pi sessions for /resume",
|
|
173
174
|
/** Create independently resumable Pi session JSONL files from current-project Codex sessions. */
|
|
174
175
|
handler: async (_args, ctx) => {
|
|
175
|
-
_notifySessionMigration(ctx, migrateCodexProjectSessions(ctx.sessionManager.getCwd()));
|
|
176
|
+
_notifySessionMigration(ctx, "Codex", migrateCodexProjectSessions(ctx.sessionManager.getCwd()));
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.registerCommand("project-claude-session-migration", {
|
|
181
|
+
description: "Convert current-project Claude Code sessions into separate native Pi sessions for /resume",
|
|
182
|
+
/** Create independently resumable Pi session JSONL files from current-project Claude Code sessions. */
|
|
183
|
+
handler: async (_args, ctx) => {
|
|
184
|
+
_notifySessionMigration(ctx, "Claude Code", migrateClaudeProjectSessions(ctx.sessionManager.getCwd()));
|
|
176
185
|
},
|
|
177
186
|
});
|
|
178
187
|
|
|
179
188
|
pi.registerTool({
|
|
180
189
|
name: "migrate_codex_project_sessions",
|
|
181
190
|
label: "Migrate Codex Project Sessions",
|
|
182
|
-
description: "Convert each Codex session for the current project into a separate native Pi session
|
|
183
|
-
promptSnippet: "
|
|
191
|
+
description: "Convert each Codex session for the current project into a separate native Pi session selectable with /resume. Call only when the user explicitly wants native Pi continuation of prior Codex work. This writes resumable sessions; it does not index history for recall_memory or manage durable memories.",
|
|
192
|
+
promptSnippet: "Use only when the user explicitly requests native Pi continuation of this project's prior Codex sessions.",
|
|
184
193
|
parameters: Type.Object({}),
|
|
185
194
|
/** Give the agent the same native Codex-to-Pi migration available through /project-session-migration. */
|
|
186
195
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
187
196
|
const stats = migrateCodexProjectSessions(ctx.sessionManager.getCwd());
|
|
188
197
|
return {
|
|
189
|
-
content: [{ type: "text" as const, text: _sessionMigrationSummary(stats) }],
|
|
198
|
+
content: [{ type: "text" as const, text: _sessionMigrationSummary("Codex", stats) }],
|
|
199
|
+
details: stats,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
pi.registerTool({
|
|
205
|
+
name: "migrate_claude_project_sessions",
|
|
206
|
+
label: "Migrate Claude Code Project Sessions",
|
|
207
|
+
description: "Convert each Claude Code session for the current project into a separate native Pi session selectable with /resume. Call only when the user explicitly wants native Pi continuation of prior Claude Code work. This writes resumable sessions; it does not index history for recall_memory or manage durable memories.",
|
|
208
|
+
promptSnippet: "Use only when the user explicitly requests native Pi continuation of this project's prior Claude Code sessions.",
|
|
209
|
+
parameters: Type.Object({}),
|
|
210
|
+
/** Give the agent an explicit Claude Code-to-Pi native-session migration capability. */
|
|
211
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
212
|
+
const stats = migrateClaudeProjectSessions(ctx.sessionManager.getCwd());
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text" as const, text: _sessionMigrationSummary("Claude Code", stats) }],
|
|
190
215
|
details: stats,
|
|
191
216
|
};
|
|
192
217
|
},
|
|
@@ -196,7 +221,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
221
|
pi.registerTool({
|
|
197
222
|
name: "recall_memory",
|
|
198
223
|
label: "Recall Memory",
|
|
199
|
-
description: `Search this user's past conversation history across Pi, Claude Code, and Codex.
|
|
224
|
+
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
225
|
|
|
201
226
|
Invocation policy:
|
|
202
227
|
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,12 +231,13 @@ Invocation policy:
|
|
|
206
231
|
- "之前那个方案怎么说的" / "what was that plan we had?"
|
|
207
232
|
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
233
|
3. Do not search history merely because a question is difficult when the user's prior discussions are not relevant.
|
|
209
|
-
4.
|
|
210
|
-
5.
|
|
234
|
+
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", "修复"].
|
|
235
|
+
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.
|
|
236
|
+
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
237
|
|
|
212
238
|
Memory-aware response policy:
|
|
213
239
|
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. A durable memory can report
|
|
240
|
+
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
241
|
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
242
|
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
243
|
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.
|
|
@@ -223,21 +249,20 @@ Session-expansion policy:
|
|
|
223
249
|
|
|
224
250
|
Pagination policy:
|
|
225
251
|
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
|
|
252
|
+
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
253
|
|
|
228
|
-
Extract 2–
|
|
254
|
+
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
255
|
promptSnippet: "Search cross-client Pi, Claude Code, and Codex history when the user asks about prior discussions or work.",
|
|
230
256
|
|
|
231
257
|
parameters: Type.Object({
|
|
232
|
-
|
|
233
|
-
entities: Type.Optional(Type.Array(
|
|
258
|
+
entities: Type.Array(
|
|
234
259
|
Type.String({ minLength: 1 }),
|
|
235
260
|
{
|
|
236
|
-
description: '
|
|
237
|
-
minItems:
|
|
261
|
+
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", "修复"].',
|
|
262
|
+
minItems: 2,
|
|
238
263
|
maxItems: 8,
|
|
239
264
|
},
|
|
240
|
-
)
|
|
265
|
+
),
|
|
241
266
|
sources: Type.Optional(Type.Array(Type.Union([
|
|
242
267
|
Type.Literal("pi"), Type.Literal("claude"), Type.Literal("codex"),
|
|
243
268
|
]))),
|
|
@@ -248,13 +273,13 @@ Extract 2–5 specific entities from the user's topic: project names, tool names
|
|
|
248
273
|
}),
|
|
249
274
|
|
|
250
275
|
/** Resolve an agent memory request into one explicit page of a fully evaluated local result set. */
|
|
251
|
-
async execute(_toolCallId, {
|
|
252
|
-
const results = recallMemories({
|
|
276
|
+
async execute(_toolCallId, { entities, sources, cwd, after, before, offset }) {
|
|
277
|
+
const results = recallMemories({ entities, sources, cwd, after, before });
|
|
253
278
|
const page = paginateRecallResults(results, offset);
|
|
254
|
-
const text = formatRecallResults(page.results, {
|
|
279
|
+
const text = formatRecallResults(page.results, { entities, sources, cwd, after, before }, page);
|
|
255
280
|
return {
|
|
256
281
|
content: [{ type: "text" as const, text }],
|
|
257
|
-
details: {
|
|
282
|
+
details: { entities, sources, cwd, after, before, offset: page.offset, pageSize: page.results.length, totalResults: page.totalResults, nextOffset: page.nextOffset },
|
|
258
283
|
};
|
|
259
284
|
},
|
|
260
285
|
});
|
|
@@ -299,13 +324,13 @@ function _notifyBackfill(ctx: ExtensionContext, stats: BackfillStats, action: st
|
|
|
299
324
|
for (const issue of stats.issues) ctx.ui.notify(`[session-memory] ${issue.error}`, "error");
|
|
300
325
|
}
|
|
301
326
|
|
|
302
|
-
/** Report native
|
|
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]
|
|
327
|
+
/** Report native source-to-Pi session migration results and isolated conversion failures. */
|
|
328
|
+
function _notifySessionMigration(ctx: ExtensionContext, sourceLabel: string, stats: ProjectSessionMigrationStats): void {
|
|
329
|
+
ctx.ui.notify(_sessionMigrationSummary(sourceLabel, stats), "info");
|
|
330
|
+
for (const issue of stats.issues) ctx.ui.notify(`[session-memory] ${sourceLabel} migration failed (${issue.path}): ${issue.error}`, "error");
|
|
306
331
|
}
|
|
307
332
|
|
|
308
333
|
/** Format a concise native-session migration result for commands and tools. */
|
|
309
|
-
function _sessionMigrationSummary(stats: ProjectSessionMigrationStats): string {
|
|
310
|
-
return `[session-memory] migrated ${stats.migratedSessions}
|
|
334
|
+
function _sessionMigrationSummary(sourceLabel: string, stats: ProjectSessionMigrationStats): string {
|
|
335
|
+
return `[session-memory] migrated ${stats.migratedSessions} ${sourceLabel} sessions (${stats.migratedMessages} messages); skipped ${stats.skippedSessions} already migrated sessions from ${stats.scannedFiles} scanned files. Use /resume to select a migrated Pi session.`;
|
|
311
336
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-session-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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"
|
package/src/helper.ts
CHANGED
|
@@ -9,15 +9,15 @@ You can request history and memory features in natural language.
|
|
|
9
9
|
无论对话来自 Pi、Claude Code 还是 Codex,你都可以直接请求回忆历史或保存、管理长期记忆。
|
|
10
10
|
Regardless of whether a conversation came from Pi, Claude Code, or Codex, you can directly recall history and save or manage durable memories.
|
|
11
11
|
|
|
12
|
-
- **在 Pi 中无缝接续 Codex 项目会话 / Seamlessly continue Codex project sessions in Pi**
|
|
13
|
-
- 只有当你明确希望在 Pi 中接续某个项目的 Codex 历史工作流时,才建议迁移项目会话。
|
|
14
|
-
- Migrate sessions only when you explicitly want to continue a project's Codex workflow seamlessly in Pi.
|
|
15
|
-
- “请把当前项目以前的 Codex 会话迁移成 Pi session。”
|
|
16
|
-
- “Convert this project's previous Codex sessions into Pi sessions.”
|
|
17
|
-
-
|
|
18
|
-
- Each
|
|
19
|
-
-
|
|
20
|
-
- Only
|
|
12
|
+
- **在 Pi 中无缝接续 Claude Code 或 Codex 项目会话 / Seamlessly continue Claude Code or Codex project sessions in Pi**
|
|
13
|
+
- 只有当你明确希望在 Pi 中接续某个项目的 Claude Code 或 Codex 历史工作流时,才建议迁移项目会话。
|
|
14
|
+
- Migrate sessions only when you explicitly want to continue a project's Claude Code or Codex workflow seamlessly in Pi.
|
|
15
|
+
- “请把当前项目以前的 Claude Code 会话迁移成 Pi session。”或“请把当前项目以前的 Codex 会话迁移成 Pi session。”
|
|
16
|
+
- “Convert this project's previous Claude Code sessions into Pi sessions.” or “Convert this project's previous Codex sessions into Pi sessions.”
|
|
17
|
+
- 每个迁移的 session 会成为一个独立的 Pi session;完成后用 \`/resume\` 选择要继续的会话。
|
|
18
|
+
- Each migrated session becomes an independent Pi session; use \`/resume\` to select the one you want to continue.
|
|
19
|
+
- 只迁移记录的工作目录与当前项目一致的会话。
|
|
20
|
+
- Only sessions whose recorded working directory matches the current project are migrated.
|
|
21
21
|
|
|
22
22
|
- **回忆以前的讨论 / Recall past discussions**
|
|
23
23
|
- “我们之前讨论过 xxx 的什么方案?”、“找一下我以前关于 xxx 的结论。”
|
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;
|
|
@@ -72,7 +72,7 @@ const RECENCY_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
|
|
|
72
72
|
|
|
73
73
|
/** Retained for compatibility with the v0.1 public retrieval helper. */
|
|
74
74
|
export function recallTurns(entities: string[]): RecallTurnResult[] {
|
|
75
|
-
return _recallTurns({
|
|
75
|
+
return _recallTurns({ entities });
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Retrieve active durable memories, then raw turns not already represented by unchanged source evidence. */
|
|
@@ -105,8 +105,8 @@ export function paginateRecallResults(results: RecallResult[], offset = 0): Reca
|
|
|
105
105
|
return { results: pageResults, offset, totalResults: results.length, nextOffset };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/** Render the exact
|
|
109
|
-
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 {
|
|
110
110
|
const lines = options ? [_formatRecallQuery(options), ""] : [];
|
|
111
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");
|
|
112
112
|
|
|
@@ -142,16 +142,15 @@ export function formatRecallResults(results: RecallResult[], options?: Pick<Reca
|
|
|
142
142
|
return lines.join("\n");
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/** Make each tool invocation auditable by showing its
|
|
146
|
-
function _formatRecallQuery(options: Pick<RecallOptions, "
|
|
147
|
-
const
|
|
148
|
-
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 = [
|
|
149
148
|
options.sources?.length ? `sources: ${options.sources.join(", ")}` : null,
|
|
150
149
|
options.cwd ? `cwd: \`${options.cwd}\`` : null,
|
|
151
150
|
options.after !== undefined ? `after: ${new Date(options.after).toISOString()}` : null,
|
|
152
151
|
options.before !== undefined ? `before: ${new Date(options.before).toISOString()}` : null,
|
|
153
152
|
].filter(Boolean);
|
|
154
|
-
return `**Search
|
|
153
|
+
return `**Search entities:** ${options.entities.map((entity) => `\`${entity}\``).join(", ")}${scope.length ? ` \\n**Scope:** ${scope.join(" · ")}` : ""}`;
|
|
155
154
|
}
|
|
156
155
|
|
|
157
156
|
/** Keep discovery results small; full persisted turn text belongs to fetch_session. */
|
|
@@ -166,7 +165,7 @@ function _recallDurableMemories(options: RecallOptions): RecallDurableMemoryResu
|
|
|
166
165
|
if (terms.length === 0) return [];
|
|
167
166
|
const scoreExpression = terms.map(() => "CASE WHEN LOWER(content) LIKE ? ESCAPE '\\' THEN 1 ELSE 0 END").join(" + ");
|
|
168
167
|
const parameters = terms.map(_likePattern);
|
|
169
|
-
const filters = terms.map(() => "LOWER(content) LIKE ? ESCAPE '\\'");
|
|
168
|
+
const filters = [`(${terms.map(() => "LOWER(content) LIKE ? ESCAPE '\\'").join(" OR ")})`];
|
|
170
169
|
const filterParameters: Array<string | number> = terms.map(_likePattern);
|
|
171
170
|
filters.push("superseded_by IS NULL");
|
|
172
171
|
if (options.cwd) {
|
|
@@ -237,7 +236,7 @@ function _recallTurns(options: RecallOptions): RecallTurnResult[] {
|
|
|
237
236
|
const scoreParameters = terms.flatMap((term) => _likeParameters(term));
|
|
238
237
|
const whereExpressions = terms.map(() => "(LOWER(turns.user_text) LIKE ? ESCAPE '\\' OR LOWER(turns.reply_text) LIKE ? ESCAPE '\\')");
|
|
239
238
|
const whereParameters = terms.flatMap((term) => _likeParameters(term));
|
|
240
|
-
const filters = [
|
|
239
|
+
const filters = [`(${whereExpressions.join(" OR ")})`];
|
|
241
240
|
const filterParameters: Array<string | number> = [...whereParameters];
|
|
242
241
|
if (options.sources?.length) {
|
|
243
242
|
filters.push(`sessions.source IN (${options.sources.map(() => "?").join(", ")})`);
|
|
@@ -272,9 +271,9 @@ function _turnContentHash(turn: RecallTurnResult): string {
|
|
|
272
271
|
return createHash("sha256").update(JSON.stringify([turn.user_text, turn.reply_text])).digest("hex");
|
|
273
272
|
}
|
|
274
273
|
|
|
275
|
-
/** 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. */
|
|
276
275
|
function _terms(options: RecallOptions): string[] {
|
|
277
|
-
return [...new Set(
|
|
276
|
+
return [...new Set(options.entities.map((entity) => entity.trim()).filter(Boolean))];
|
|
278
277
|
}
|
|
279
278
|
|
|
280
279
|
/** Produce matching user and assistant SQL LIKE parameters for one term. */
|
package/src/session-migration.ts
CHANGED
|
@@ -3,18 +3,28 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSyn
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type MigrationSource = "claude" | "codex";
|
|
7
|
+
type Role = "user" | "assistant";
|
|
8
|
+
|
|
9
|
+
interface MigratedMessage {
|
|
7
10
|
id: string;
|
|
8
|
-
role:
|
|
11
|
+
role: Role;
|
|
9
12
|
text: string;
|
|
10
13
|
timestamp: number;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
interface
|
|
16
|
+
interface SourceSession {
|
|
14
17
|
id: string;
|
|
15
18
|
cwd: string;
|
|
16
19
|
timestamp: number;
|
|
17
|
-
messages:
|
|
20
|
+
messages: MigratedMessage[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MigrationDefinition {
|
|
24
|
+
source: MigrationSource;
|
|
25
|
+
root: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
parse: (path: string) => SourceSession | undefined;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export interface ProjectSessionMigrationStats {
|
|
@@ -25,8 +35,33 @@ export interface ProjectSessionMigrationStats {
|
|
|
25
35
|
issues: Array<{ path: string; error: string }>;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
const MIGRATION_SOURCES: Record<MigrationSource, MigrationDefinition> = {
|
|
39
|
+
claude: {
|
|
40
|
+
source: "claude",
|
|
41
|
+
root: join(homedir(), ".claude", "projects"),
|
|
42
|
+
displayName: "Claude Code",
|
|
43
|
+
parse: _parseClaudeSession,
|
|
44
|
+
},
|
|
45
|
+
codex: {
|
|
46
|
+
source: "codex",
|
|
47
|
+
root: join(homedir(), ".codex", "sessions"),
|
|
48
|
+
displayName: "Codex",
|
|
49
|
+
parse: _parseCodexSession,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Convert every current-project Claude Code session into an independently resumable Pi session file. */
|
|
54
|
+
export function migrateClaudeProjectSessions(cwd: string): ProjectSessionMigrationStats {
|
|
55
|
+
return _migrateProjectSessions(MIGRATION_SOURCES.claude, cwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Convert every current-project Codex session into an independently resumable Pi session file. */
|
|
29
59
|
export function migrateCodexProjectSessions(cwd: string): ProjectSessionMigrationStats {
|
|
60
|
+
return _migrateProjectSessions(MIGRATION_SOURCES.codex, cwd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Migrate one supported source while isolating malformed files from other source sessions. */
|
|
64
|
+
function _migrateProjectSessions(definition: MigrationDefinition, cwd: string): ProjectSessionMigrationStats {
|
|
30
65
|
const stats: ProjectSessionMigrationStats = {
|
|
31
66
|
scannedFiles: 0,
|
|
32
67
|
migratedSessions: 0,
|
|
@@ -34,17 +69,17 @@ export function migrateCodexProjectSessions(cwd: string): ProjectSessionMigratio
|
|
|
34
69
|
migratedMessages: 0,
|
|
35
70
|
issues: [],
|
|
36
71
|
};
|
|
37
|
-
for (const path of _jsonlFiles(
|
|
72
|
+
for (const path of _jsonlFiles(definition.root)) {
|
|
38
73
|
stats.scannedFiles++;
|
|
39
74
|
try {
|
|
40
|
-
const session =
|
|
75
|
+
const session = definition.parse(path);
|
|
41
76
|
if (!session || session.cwd !== cwd) continue;
|
|
42
|
-
const outputPath = _targetPath(session);
|
|
77
|
+
const outputPath = _targetPath(definition.source, session);
|
|
43
78
|
if (existsSync(outputPath)) {
|
|
44
79
|
stats.skippedSessions++;
|
|
45
80
|
continue;
|
|
46
81
|
}
|
|
47
|
-
_writePiSession(session, outputPath);
|
|
82
|
+
_writePiSession(definition, session, outputPath);
|
|
48
83
|
stats.migratedSessions++;
|
|
49
84
|
stats.migratedMessages += session.messages.length;
|
|
50
85
|
} catch (error) {
|
|
@@ -54,9 +89,40 @@ export function migrateCodexProjectSessions(cwd: string): ProjectSessionMigratio
|
|
|
54
89
|
return stats;
|
|
55
90
|
}
|
|
56
91
|
|
|
57
|
-
/** Convert one
|
|
58
|
-
function
|
|
59
|
-
const entries =
|
|
92
|
+
/** Convert one Claude Code JSONL file into supported textual user and assistant messages. */
|
|
93
|
+
function _parseClaudeSession(path: string): SourceSession | undefined {
|
|
94
|
+
const entries = _readJsonl(path);
|
|
95
|
+
const firstConversation = entries.find((entry) =>
|
|
96
|
+
(entry.type === "user" || entry.type === "assistant") && !entry.isMeta && !entry.isSidechain,
|
|
97
|
+
);
|
|
98
|
+
if (!firstConversation) return undefined;
|
|
99
|
+
const id = _string(firstConversation.sessionId);
|
|
100
|
+
const cwd = _string(firstConversation.cwd);
|
|
101
|
+
const timestamp = _timestamp(_string(firstConversation.timestamp));
|
|
102
|
+
if (!id || !cwd || timestamp === undefined) throw new Error("Claude Code conversation requires sessionId, cwd, and timestamp");
|
|
103
|
+
|
|
104
|
+
const messages: MigratedMessage[] = [];
|
|
105
|
+
for (const [index, entry] of entries.entries()) {
|
|
106
|
+
if ((entry.type !== "user" && entry.type !== "assistant") || entry.isMeta || entry.isSidechain) continue;
|
|
107
|
+
const role = _role(entry.message?.role);
|
|
108
|
+
if (!role) continue;
|
|
109
|
+
const text = _claudeText(entry.message?.content);
|
|
110
|
+
if (!text || (role === "user" && _isClaudeInjectedContext(text))) continue;
|
|
111
|
+
const messageId = _string(entry.uuid) ?? _string(entry.id);
|
|
112
|
+
if (!messageId) throw new Error(`Claude Code textual message at entry ${index} has no stable native ID`);
|
|
113
|
+
messages.push({
|
|
114
|
+
id: messageId,
|
|
115
|
+
role,
|
|
116
|
+
text,
|
|
117
|
+
timestamp: _timestamp(_string(entry.timestamp)) ?? timestamp,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return { id, cwd, timestamp, messages };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Convert one Codex JSONL file into supported textual user and assistant messages. */
|
|
124
|
+
function _parseCodexSession(path: string): SourceSession | undefined {
|
|
125
|
+
const entries = _readJsonl(path);
|
|
60
126
|
const metadata = entries.find((entry) => entry.type === "session_meta")?.payload as Record<string, unknown> | undefined;
|
|
61
127
|
if (!metadata) return undefined;
|
|
62
128
|
const id = _string(metadata.session_id) ?? _string(metadata.id);
|
|
@@ -64,14 +130,14 @@ function _parseCodexSession(path: string): CodexSession | undefined {
|
|
|
64
130
|
const timestamp = _timestamp(_string(metadata.timestamp));
|
|
65
131
|
if (!id || !cwd || timestamp === undefined) throw new Error("Codex session_meta requires session_id/id, cwd, and timestamp");
|
|
66
132
|
|
|
67
|
-
const messages:
|
|
133
|
+
const messages: MigratedMessage[] = [];
|
|
68
134
|
for (const [index, entry] of entries.entries()) {
|
|
69
135
|
if (entry.type !== "response_item") continue;
|
|
70
136
|
const payload = entry.payload as Record<string, unknown> | undefined;
|
|
71
137
|
if (payload?.type !== "message") continue;
|
|
72
|
-
const role =
|
|
73
|
-
if (role
|
|
74
|
-
const text =
|
|
138
|
+
const role = _role(payload.role);
|
|
139
|
+
if (!role) continue;
|
|
140
|
+
const text = _codexText(payload.content);
|
|
75
141
|
if (!text) continue;
|
|
76
142
|
const messageId = _string(payload.id)
|
|
77
143
|
?? _string(entry.id)
|
|
@@ -87,8 +153,8 @@ function _parseCodexSession(path: string): CodexSession | undefined {
|
|
|
87
153
|
return { id, cwd, timestamp, messages };
|
|
88
154
|
}
|
|
89
155
|
|
|
90
|
-
/** Write
|
|
91
|
-
function _writePiSession(session:
|
|
156
|
+
/** Write one valid Pi v3 session with a linear message branch and explicit migration provenance. */
|
|
157
|
+
function _writePiSession(definition: MigrationDefinition, session: SourceSession, path: string): void {
|
|
92
158
|
mkdirSync(join(homedir(), ".pi", "agent", "sessions", _encodedCwd(session.cwd)), { recursive: true });
|
|
93
159
|
const lines: string[] = [JSON.stringify({
|
|
94
160
|
type: "session",
|
|
@@ -98,17 +164,17 @@ function _writePiSession(session: CodexSession, path: string): void {
|
|
|
98
164
|
cwd: session.cwd,
|
|
99
165
|
})];
|
|
100
166
|
let parentId: string | null = null;
|
|
101
|
-
const nameId = _entryId(session.id, "name");
|
|
167
|
+
const nameId = _entryId(definition.source, session.id, "name");
|
|
102
168
|
lines.push(JSON.stringify({
|
|
103
169
|
type: "session_info",
|
|
104
170
|
id: nameId,
|
|
105
171
|
parentId,
|
|
106
172
|
timestamp: new Date(session.timestamp).toISOString(),
|
|
107
|
-
name: `Migrated from
|
|
173
|
+
name: `Migrated from ${definition.displayName}: ${session.id}`,
|
|
108
174
|
}));
|
|
109
175
|
parentId = nameId;
|
|
110
176
|
for (const message of session.messages) {
|
|
111
|
-
const id = _entryId(session.id, message.id);
|
|
177
|
+
const id = _entryId(definition.source, session.id, message.id);
|
|
112
178
|
const timestamp = new Date(message.timestamp).toISOString();
|
|
113
179
|
lines.push(JSON.stringify({
|
|
114
180
|
type: "message",
|
|
@@ -120,8 +186,8 @@ function _writePiSession(session: CodexSession, path: string): void {
|
|
|
120
186
|
: {
|
|
121
187
|
role: "assistant",
|
|
122
188
|
content: [{ type: "text", text: message.text }],
|
|
123
|
-
api:
|
|
124
|
-
provider:
|
|
189
|
+
api: `${definition.source}-migration`,
|
|
190
|
+
provider: definition.source,
|
|
125
191
|
model: "unknown",
|
|
126
192
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
127
193
|
stopReason: "stop",
|
|
@@ -139,9 +205,9 @@ function _writePiSession(session: CodexSession, path: string): void {
|
|
|
139
205
|
}
|
|
140
206
|
}
|
|
141
207
|
|
|
142
|
-
/** Return the deterministic Pi session-file location for one
|
|
143
|
-
function _targetPath(session:
|
|
144
|
-
return join(homedir(), ".pi", "agent", "sessions", _encodedCwd(session.cwd), `${new Date(session.timestamp).toISOString().replace(/[.:]/g, "-")}
|
|
208
|
+
/** Return the deterministic Pi session-file location for one migrated source session. */
|
|
209
|
+
function _targetPath(source: MigrationSource, session: SourceSession): string {
|
|
210
|
+
return join(homedir(), ".pi", "agent", "sessions", _encodedCwd(session.cwd), `${new Date(session.timestamp).toISOString().replace(/[.:]/g, "-")}_${source}-${session.id}.jsonl`);
|
|
145
211
|
}
|
|
146
212
|
|
|
147
213
|
/** Encode cwd exactly as Pi's default session directory convention. */
|
|
@@ -149,9 +215,14 @@ function _encodedCwd(cwd: string): string {
|
|
|
149
215
|
return `--${cwd.split("/").filter(Boolean).join("-")}--`;
|
|
150
216
|
}
|
|
151
217
|
|
|
152
|
-
/** Create stable
|
|
153
|
-
function _entryId(sessionId: string, sourceId: string): string {
|
|
154
|
-
return createHash("sha256").update(`${sessionId}:${sourceId}`).digest("hex").slice(0, 16);
|
|
218
|
+
/** Create stable Pi-safe entry IDs while retaining source-specific identity namespaces. */
|
|
219
|
+
function _entryId(source: MigrationSource, sessionId: string, sourceId: string): string {
|
|
220
|
+
return createHash("sha256").update(`${source}:${sessionId}:${sourceId}`).digest("hex").slice(0, 16);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Accept user and assistant roles only. */
|
|
224
|
+
function _role(value: unknown): Role | undefined {
|
|
225
|
+
return value === "user" || value === "assistant" ? value : undefined;
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
/** Extract non-empty string values only. */
|
|
@@ -166,8 +237,28 @@ function _timestamp(value: string | undefined): number | undefined {
|
|
|
166
237
|
return Number.isNaN(timestamp) ? undefined : timestamp;
|
|
167
238
|
}
|
|
168
239
|
|
|
240
|
+
/** Extract Claude Code text from legacy string or text content blocks. */
|
|
241
|
+
function _claudeText(content: unknown): string {
|
|
242
|
+
if (typeof content === "string") return content.trim();
|
|
243
|
+
if (!Array.isArray(content)) return "";
|
|
244
|
+
return content
|
|
245
|
+
.filter((block): block is { type: string; text: string } => Boolean(block) && typeof block === "object" && (block as { type?: unknown }).type === "text" && typeof (block as { text?: unknown }).text === "string")
|
|
246
|
+
.map((block) => block.text)
|
|
247
|
+
.join("\n")
|
|
248
|
+
.trim();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Exclude Claude Code client-injected context from migrated user conversation. */
|
|
252
|
+
function _isClaudeInjectedContext(text: string): boolean {
|
|
253
|
+
return text.startsWith("<command-name>")
|
|
254
|
+
|| text.startsWith("<command-message>")
|
|
255
|
+
|| text.startsWith("<local-command-")
|
|
256
|
+
|| text.startsWith("<task-notification>")
|
|
257
|
+
|| text.startsWith("This session is being continued from a previous conversation");
|
|
258
|
+
}
|
|
259
|
+
|
|
169
260
|
/** Join supported Codex text content blocks. */
|
|
170
|
-
function
|
|
261
|
+
function _codexText(content: unknown): string {
|
|
171
262
|
if (!Array.isArray(content)) return "";
|
|
172
263
|
return content
|
|
173
264
|
.filter((block): block is { type: string; text: string } => Boolean(block) && typeof block === "object" && typeof (block as { type?: unknown }).type === "string" && typeof (block as { text?: unknown }).text === "string")
|
|
@@ -177,6 +268,11 @@ function _text(content: unknown): string {
|
|
|
177
268
|
.trim();
|
|
178
269
|
}
|
|
179
270
|
|
|
271
|
+
/** Read every non-empty JSONL line into its ordered JSON record. */
|
|
272
|
+
function _readJsonl(path: string): Record<string, any>[] {
|
|
273
|
+
return readFileSync(path, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line) as Record<string, any>);
|
|
274
|
+
}
|
|
275
|
+
|
|
180
276
|
/** Recursively enumerate source JSONL files. */
|
|
181
277
|
function _jsonlFiles(root: string): string[] {
|
|
182
278
|
if (!existsSync(root)) return [];
|