@ogulcancelik/pi-session-recall 1.0.4 → 1.0.6
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 +3 -3
- package/package.json +1 -1
- package/session-recall.ts +81 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-session-recall
|
|
2
2
|
|
|
3
|
-
Recall past [pi](https://github.com/
|
|
3
|
+
Recall past [pi](https://github.com/earendil-works/pi) sessions. Search through conversation history and query specific sessions with an LLM.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ Spaces mean exact spaces in an exact phrase. For unrelated concepts, the agent s
|
|
|
28
28
|
|
|
29
29
|
### `session_query`
|
|
30
30
|
|
|
31
|
-
Deep-dives into a specific session file. Loads the conversation, sends it to an LLM, and answers your question about it.
|
|
31
|
+
Deep-dives into a specific session file. Loads the conversation, sends it to an LLM, and answers your question about it. It includes user and assistant text plus tool calls, while omitting assistant thinking and tool results to keep queries efficient.
|
|
32
32
|
|
|
33
33
|
For large sessions that exceed the model's context window, it uses smart windowing: keeps the first/last messages plus keyword-relevant sections, marking gaps with `[... N messages omitted ...]`.
|
|
34
34
|
|
|
@@ -55,7 +55,7 @@ If no model is configured (or the configured model isn't available), it falls ba
|
|
|
55
55
|
|
|
56
56
|
## Requirements
|
|
57
57
|
|
|
58
|
-
- [pi](https://github.com/
|
|
58
|
+
- [pi](https://github.com/earendil-works/pi) v0.40+
|
|
59
59
|
- [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) — recommended for fast search, falls back to `grep` or Node-native scan
|
|
60
60
|
|
|
61
61
|
## License
|
package/package.json
CHANGED
package/session-recall.ts
CHANGED
|
@@ -23,8 +23,14 @@
|
|
|
23
23
|
* session_query(path, "what approach did we take for X?")
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
cleanupSessionResources,
|
|
28
|
+
complete,
|
|
29
|
+
type Model,
|
|
30
|
+
type Api,
|
|
31
|
+
type Message,
|
|
32
|
+
} from "@earendil-works/pi-ai";
|
|
33
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
28
34
|
import {
|
|
29
35
|
getAgentDir,
|
|
30
36
|
SessionManager,
|
|
@@ -32,7 +38,7 @@ import {
|
|
|
32
38
|
getMarkdownTheme,
|
|
33
39
|
serializeConversation,
|
|
34
40
|
type SessionEntry,
|
|
35
|
-
} from "@
|
|
41
|
+
} from "@earendil-works/pi-coding-agent";
|
|
36
42
|
import {
|
|
37
43
|
Container,
|
|
38
44
|
Input,
|
|
@@ -42,9 +48,10 @@ import {
|
|
|
42
48
|
type SelectListTheme,
|
|
43
49
|
Spacer,
|
|
44
50
|
Text,
|
|
45
|
-
} from "@
|
|
51
|
+
} from "@earendil-works/pi-tui";
|
|
46
52
|
import { Type } from "@sinclair/typebox";
|
|
47
53
|
import { execFileSync } from "node:child_process";
|
|
54
|
+
import { randomBytes } from "node:crypto";
|
|
48
55
|
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
|
|
49
56
|
import { join, dirname } from "node:path";
|
|
50
57
|
import { homedir } from "node:os";
|
|
@@ -174,6 +181,63 @@ function estimateTokens(text: string): number {
|
|
|
174
181
|
return Math.ceil(text.length / 4);
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
/** Create a UUIDv7 for Codex request routing, which is unreliable with UUIDv4. */
|
|
185
|
+
function createUuidV7(): string {
|
|
186
|
+
const bytes = randomBytes(16);
|
|
187
|
+
const timestamp = BigInt(Date.now());
|
|
188
|
+
|
|
189
|
+
bytes[0] = Number((timestamp >> 40n) & 0xffn);
|
|
190
|
+
bytes[1] = Number((timestamp >> 32n) & 0xffn);
|
|
191
|
+
bytes[2] = Number((timestamp >> 24n) & 0xffn);
|
|
192
|
+
bytes[3] = Number((timestamp >> 16n) & 0xffn);
|
|
193
|
+
bytes[4] = Number((timestamp >> 8n) & 0xffn);
|
|
194
|
+
bytes[5] = Number(timestamp & 0xffn);
|
|
195
|
+
bytes[6] = 0x70 | (bytes[6] & 0x0f);
|
|
196
|
+
bytes[8] = 0x80 | (bytes[8] & 0x3f);
|
|
197
|
+
|
|
198
|
+
const hex = bytes.toString("hex");
|
|
199
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function completeRecallRequest(
|
|
203
|
+
model: Model<Api>,
|
|
204
|
+
context: Parameters<typeof complete>[1],
|
|
205
|
+
options: NonNullable<Parameters<typeof complete>[2]>,
|
|
206
|
+
): ReturnType<typeof complete> {
|
|
207
|
+
if (model.provider !== "openai-codex") return complete(model, context, options);
|
|
208
|
+
|
|
209
|
+
const sessionId = createUuidV7();
|
|
210
|
+
try {
|
|
211
|
+
return await complete(model, context, {
|
|
212
|
+
...options,
|
|
213
|
+
sessionId,
|
|
214
|
+
transport: "websocket",
|
|
215
|
+
});
|
|
216
|
+
} finally {
|
|
217
|
+
cleanupSessionResources(sessionId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Keep the conversational record and actions while dropping the two largest,
|
|
223
|
+
* least useful sources of recall context: assistant thinking and tool output.
|
|
224
|
+
*/
|
|
225
|
+
function prepareRecallMessages(messages: Message[]): Message[] {
|
|
226
|
+
const prepared: Message[] = [];
|
|
227
|
+
|
|
228
|
+
for (const message of messages) {
|
|
229
|
+
if (message.role === "toolResult") continue;
|
|
230
|
+
if (message.role === "assistant") {
|
|
231
|
+
const content = message.content.filter((block) => block.type !== "thinking");
|
|
232
|
+
if (content.length > 0) prepared.push({ ...message, content });
|
|
233
|
+
} else {
|
|
234
|
+
prepared.push(message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return prepared;
|
|
239
|
+
}
|
|
240
|
+
|
|
177
241
|
/**
|
|
178
242
|
* Extract keywords from a question for matching.
|
|
179
243
|
* Strips common stop words and short words.
|
|
@@ -795,7 +859,7 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
795
859
|
label: (params) => `Session Query: ${params.question}`,
|
|
796
860
|
description:
|
|
797
861
|
"Query a specific session file to get detailed information. Use after session_search to dig into a particular session, " +
|
|
798
|
-
"or when you already have a session path (e.g., from a handoff). Sends the
|
|
862
|
+
"or when you already have a session path (e.g., from a handoff). Sends the conversation and tool calls, without assistant thinking or tool output, to an LLM for analysis.",
|
|
799
863
|
renderResult: (result, options, theme) => {
|
|
800
864
|
const container = new Container();
|
|
801
865
|
const text = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
@@ -897,7 +961,7 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
897
961
|
}
|
|
898
962
|
|
|
899
963
|
// Serialize and check if we need windowing
|
|
900
|
-
const llmMessages = convertToLlm(messages);
|
|
964
|
+
const llmMessages = prepareRecallMessages(convertToLlm(messages));
|
|
901
965
|
const fullText = serializeConversation(llmMessages);
|
|
902
966
|
const fullTokens = estimateTokens(fullText);
|
|
903
967
|
|
|
@@ -945,7 +1009,7 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
945
1009
|
timestamp: Date.now(),
|
|
946
1010
|
};
|
|
947
1011
|
|
|
948
|
-
const response = await
|
|
1012
|
+
const response = await completeRecallRequest(
|
|
949
1013
|
queryModel,
|
|
950
1014
|
{ systemPrompt: QUERY_SYSTEM_PROMPT, messages: [userMessage] },
|
|
951
1015
|
{ apiKey: auth.apiKey, headers: auth.headers, signal },
|
|
@@ -960,9 +1024,17 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
960
1024
|
};
|
|
961
1025
|
}
|
|
962
1026
|
|
|
963
|
-
if (response.stopReason === "error"
|
|
1027
|
+
if (response.stopReason === "error") {
|
|
1028
|
+
const providerError =
|
|
1029
|
+
response.errorMessage?.trim() || "The provider returned an error without details.";
|
|
1030
|
+
return errorResult(
|
|
1031
|
+
`Error querying ${queryModel.provider}/${queryModel.id}: ${providerError}`,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (response.content.length === 0) {
|
|
964
1036
|
return errorResult(
|
|
965
|
-
`Error
|
|
1037
|
+
`Error querying ${queryModel.provider}/${queryModel.id}: LLM returned an empty response.`,
|
|
966
1038
|
);
|
|
967
1039
|
}
|
|
968
1040
|
|