@ssweens/pi-compaxxt 1.0.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/LICENSE +21 -0
- package/README.md +78 -0
- package/extensions/compaction.ts +209 -0
- package/extensions/session-query.ts +219 -0
- package/package.json +31 -0
- package/skills/pi-session-query/SKILL.md +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ssweens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# pi-compaxxt
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pi install @ssweens/pi-compaxxt
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Enhanced compaction for [pi](https://github.com/badlogic/pi-mono). Two improvements to every compaction, zero extra LLM calls.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Session Context
|
|
12
|
+
|
|
13
|
+
Every compaction summary is prepended with the current session file path and thread ID:
|
|
14
|
+
|
|
15
|
+
```markdown
|
|
16
|
+
## Session Context
|
|
17
|
+
**Session:** `/Users/you/.pi/agent/sessions/abc123/session.jsonl`
|
|
18
|
+
**Thread ID:** `e7f2a3c1-...`
|
|
19
|
+
|
|
20
|
+
Use the `session_query` tool to retrieve specific context from messages that were summarized away.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Goal
|
|
25
|
+
...
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
After compaction, the LLM knows exactly where to look if it needs older context that was summarized away. The bundled `session_query` tool makes this retrieval cheap — it doesn't load the full session, just answers a targeted question from it.
|
|
29
|
+
|
|
30
|
+
### LLM-Judged Important Files
|
|
31
|
+
|
|
32
|
+
The compaction prompt is augmented to ask the LLM to identify the most goal-relevant files as part of generating the summary. The file sections are restructured:
|
|
33
|
+
|
|
34
|
+
```xml
|
|
35
|
+
<important-files>
|
|
36
|
+
src/core/compaction.ts
|
|
37
|
+
extensions/handoff.ts
|
|
38
|
+
</important-files>
|
|
39
|
+
|
|
40
|
+
<modified-files>
|
|
41
|
+
src/core/compaction.ts
|
|
42
|
+
extensions/handoff.ts
|
|
43
|
+
src/utils.ts
|
|
44
|
+
</modified-files>
|
|
45
|
+
|
|
46
|
+
<other-read-files>
|
|
47
|
+
package.json
|
|
48
|
+
docs/api.md
|
|
49
|
+
</other-read-files>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Files are selected using these criteria:
|
|
53
|
+
- Directly related to accomplishing the goal
|
|
54
|
+
- Contain reference code or patterns to follow
|
|
55
|
+
- Will need to be read, edited, or created
|
|
56
|
+
- Provide important context or constraints
|
|
57
|
+
|
|
58
|
+
`<modified-files>` is left untouched (intentional overlap with important-files is fine). `<read-files>` becomes `<other-read-files>` with the important ones pruned out.
|
|
59
|
+
|
|
60
|
+
If the LLM doesn't output a parseable `## Most Important Files` section, the extension falls back to the default `<read-files>`/`<modified-files>` format silently.
|
|
61
|
+
|
|
62
|
+
## Components
|
|
63
|
+
|
|
64
|
+
| Component | Type | Description |
|
|
65
|
+
|-----------|------|-------------|
|
|
66
|
+
| [compaction.ts](extensions/compaction.ts) | Extension | `session_before_compact` hook — session context + file restructuring |
|
|
67
|
+
| [session-query.ts](extensions/session-query.ts) | Extension | `session_query` tool for querying session history |
|
|
68
|
+
| [pi-session-query/](skills/pi-session-query/SKILL.md) | Skill | Instructions for using `session_query` |
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- If you also have `@ssweens/pi-handoff` installed, both packages register the `session_query` tool. Pi will warn about the duplicate — it's harmless, one will shadow the other.
|
|
73
|
+
- Works with `/compact [instructions]` — user instructions are preserved and the file importance prompt is appended after them.
|
|
74
|
+
- On any compaction error, falls back to pi's default compaction silently (with a warning notification).
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi-Compaxxt Compaction Extension
|
|
3
|
+
*
|
|
4
|
+
* Enhances pi's default compaction with two features:
|
|
5
|
+
*
|
|
6
|
+
* 1. Session context block prepended to every summary — session file path and
|
|
7
|
+
* thread ID so the post-compaction LLM can use session_query to retrieve
|
|
8
|
+
* older context that was summarized away.
|
|
9
|
+
*
|
|
10
|
+
* 2. LLM-judged <important-files> section — the compaction prompt is augmented
|
|
11
|
+
* to ask the LLM to identify the most goal-relevant files as part of
|
|
12
|
+
* generating the summary (one LLM call, no extra cost). The file sections
|
|
13
|
+
* are then restructured:
|
|
14
|
+
* <important-files> — LLM-ranked top 3-5 files
|
|
15
|
+
* <modified-files> — all modified files, unchanged from default
|
|
16
|
+
* <other-read-files> — read-only files minus the important ones
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { compact } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Session context block
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function buildSessionContextBlock(
|
|
27
|
+
sessionFile: string | undefined,
|
|
28
|
+
leafId: string | null,
|
|
29
|
+
): string {
|
|
30
|
+
const lines: string[] = ["## Session Context"];
|
|
31
|
+
if (sessionFile) lines.push(`**Session:** \`${sessionFile}\``);
|
|
32
|
+
if (leafId) lines.push(`**Thread ID:** \`${leafId}\``);
|
|
33
|
+
lines.push(
|
|
34
|
+
"",
|
|
35
|
+
"Use the `session_query` tool to retrieve specific context from messages that were summarized away.",
|
|
36
|
+
"",
|
|
37
|
+
"---",
|
|
38
|
+
"",
|
|
39
|
+
);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Important files: parsing + file section restructuring
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse the ## Most Important Files section from the LLM summary.
|
|
49
|
+
* Validates each path against the known file list to guard against hallucination.
|
|
50
|
+
*/
|
|
51
|
+
function parseImportantFiles(summary: string, knownFiles: Set<string>): string[] {
|
|
52
|
+
const match = summary.match(/\n## Most Important Files\n([\s\S]+?)(?=\n\n<|$)/);
|
|
53
|
+
if (!match) return [];
|
|
54
|
+
|
|
55
|
+
return match[1]
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((line) => line.replace(/^[-*]\s*/, "").trim()) // strip bullets if LLM added them
|
|
58
|
+
.map((line) => line.split(/\s+/)[0]) // strip any inline explanation after the path
|
|
59
|
+
.filter((path) => path.length > 0 && knownFiles.has(path));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Restructure the file XML sections in the summary:
|
|
64
|
+
* - Remove ## Most Important Files from markdown (now encoded in XML)
|
|
65
|
+
* - Remove <read-files> and replace with <other-read-files> (pruned)
|
|
66
|
+
* - Insert <important-files> before <modified-files>
|
|
67
|
+
* - Leave <modified-files> untouched (may overlap with important-files — intentional)
|
|
68
|
+
*/
|
|
69
|
+
function restructureFileSections(
|
|
70
|
+
summary: string,
|
|
71
|
+
importantFiles: string[],
|
|
72
|
+
readFiles: string[],
|
|
73
|
+
): string {
|
|
74
|
+
const importantSet = new Set(importantFiles);
|
|
75
|
+
|
|
76
|
+
// Remove the ## Most Important Files markdown section
|
|
77
|
+
let result = summary.replace(/\n## Most Important Files\n[\s\S]+?(?=\n\n<|$)/, "");
|
|
78
|
+
|
|
79
|
+
// Remove the existing <read-files> block entirely
|
|
80
|
+
result = result.replace(/\n\n<read-files>\n[\s\S]+?\n<\/read-files>/, "");
|
|
81
|
+
|
|
82
|
+
// Compute other-read-files: read-only files not in the important list
|
|
83
|
+
const otherReadFiles = readFiles.filter((f) => !importantSet.has(f));
|
|
84
|
+
|
|
85
|
+
const importantSection = `<important-files>\n${importantFiles.join("\n")}\n</important-files>`;
|
|
86
|
+
const otherReadSection =
|
|
87
|
+
otherReadFiles.length > 0
|
|
88
|
+
? `<other-read-files>\n${otherReadFiles.join("\n")}\n</other-read-files>`
|
|
89
|
+
: "";
|
|
90
|
+
|
|
91
|
+
// Insert <important-files> before <modified-files> if present, otherwise append
|
|
92
|
+
if (result.includes("<modified-files>")) {
|
|
93
|
+
result = result.replace(
|
|
94
|
+
"\n\n<modified-files>",
|
|
95
|
+
`\n\n${importantSection}\n\n<modified-files>`,
|
|
96
|
+
);
|
|
97
|
+
if (otherReadSection) {
|
|
98
|
+
result = result.replace("</modified-files>", `</modified-files>\n\n${otherReadSection}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Read-only session — no modified-files section
|
|
102
|
+
result += `\n\n${importantSection}`;
|
|
103
|
+
if (otherReadSection) result += `\n\n${otherReadSection}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Extension
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
export default function (pi: ExtensionAPI) {
|
|
114
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
115
|
+
if (!ctx.model) return;
|
|
116
|
+
|
|
117
|
+
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
|
118
|
+
if (!apiKey) return;
|
|
119
|
+
|
|
120
|
+
const { preparation, customInstructions: userInstructions, signal } = event;
|
|
121
|
+
|
|
122
|
+
// Equivalent to computeFileLists() from compaction/utils — not re-exported by the package
|
|
123
|
+
const modified = new Set([...preparation.fileOps.edited, ...preparation.fileOps.written]);
|
|
124
|
+
const readFiles = [...preparation.fileOps.read].filter((f) => !modified.has(f)).sort();
|
|
125
|
+
const modifiedFiles = [...modified].sort();
|
|
126
|
+
const allFiles = [...readFiles, ...modifiedFiles];
|
|
127
|
+
|
|
128
|
+
// Build file importance instruction, respecting any user /compact [instructions]
|
|
129
|
+
let combinedInstructions = userInstructions ?? "";
|
|
130
|
+
|
|
131
|
+
if (allFiles.length > 0) {
|
|
132
|
+
const fileLines: string[] = [];
|
|
133
|
+
if (modifiedFiles.length > 0) {
|
|
134
|
+
fileLines.push(`Modified:\n${modifiedFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
135
|
+
}
|
|
136
|
+
if (readFiles.length > 0) {
|
|
137
|
+
fileLines.push(`Read-only:\n${readFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileImportanceInstruction = `After all other sections, add:
|
|
141
|
+
|
|
142
|
+
## Most Important Files
|
|
143
|
+
Identify files that are:
|
|
144
|
+
- Directly related to accomplishing the goal
|
|
145
|
+
- Contain reference code or patterns to follow
|
|
146
|
+
- Will need to be read, edited, or created
|
|
147
|
+
- Provide important context or constraints
|
|
148
|
+
|
|
149
|
+
List 3-5 files from those accessed this session, most important first.
|
|
150
|
+
One path per line, no bullets, no explanation.
|
|
151
|
+
|
|
152
|
+
All files accessed this session:
|
|
153
|
+
${fileLines.join("\n\n")}`;
|
|
154
|
+
|
|
155
|
+
combinedInstructions = combinedInstructions
|
|
156
|
+
? `${combinedInstructions}\n\n${fileImportanceInstruction}`
|
|
157
|
+
: fileImportanceInstruction;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await compact(
|
|
162
|
+
preparation,
|
|
163
|
+
ctx.model,
|
|
164
|
+
apiKey,
|
|
165
|
+
combinedInstructions || undefined,
|
|
166
|
+
signal,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (signal.aborted) return;
|
|
170
|
+
|
|
171
|
+
let summary = result.summary;
|
|
172
|
+
|
|
173
|
+
// Parse and restructure file sections
|
|
174
|
+
if (allFiles.length > 0) {
|
|
175
|
+
const knownFiles = new Set(allFiles);
|
|
176
|
+
const importantFiles = parseImportantFiles(summary, knownFiles);
|
|
177
|
+
if (importantFiles.length > 0) {
|
|
178
|
+
summary = restructureFileSections(summary, importantFiles, readFiles);
|
|
179
|
+
}
|
|
180
|
+
// If LLM didn't follow the format, summary falls back to default
|
|
181
|
+
// <read-files>/<modified-files> sections untouched
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Prepend session context block
|
|
185
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
186
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
187
|
+
if (sessionFile || leafId) {
|
|
188
|
+
summary = buildSessionContextBlock(sessionFile, leafId) + summary;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
compaction: {
|
|
193
|
+
summary,
|
|
194
|
+
firstKeptEntryId: result.firstKeptEntryId,
|
|
195
|
+
tokensBefore: result.tokensBefore,
|
|
196
|
+
details: result.details,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (!signal.aborted) {
|
|
201
|
+
ctx.ui.notify(
|
|
202
|
+
`pi-compaxxt: compaction failed, using default. ${err instanceof Error ? err.message : String(err)}`,
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return; // fall back to default compaction
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Query Extension - Query previous pi sessions
|
|
3
|
+
*
|
|
4
|
+
* Provides a tool the model can use to query past sessions for context,
|
|
5
|
+
* decisions, code changes, or other information.
|
|
6
|
+
*
|
|
7
|
+
* Works with handoff: when a handoff prompt includes "Parent session: <path>",
|
|
8
|
+
* the model can use this tool to look up details from that session.
|
|
9
|
+
*
|
|
10
|
+
* Based on pi-amplike's session-query, enhanced with:
|
|
11
|
+
* - Better error handling
|
|
12
|
+
* - Rendered results with markdown support
|
|
13
|
+
* - Session metadata in response
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { complete, type Message } from "@mariozechner/pi-ai";
|
|
17
|
+
import type { ExtensionAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import {
|
|
19
|
+
SessionManager,
|
|
20
|
+
convertToLlm,
|
|
21
|
+
serializeConversation,
|
|
22
|
+
getMarkdownTheme,
|
|
23
|
+
} from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
25
|
+
import { Type } from "@sinclair/typebox";
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
|
|
28
|
+
// Maximum characters of serialized conversation to send to the query LLM.
|
|
29
|
+
// Prevents blowing context when the parent session is very large.
|
|
30
|
+
// ~100k chars ≈ ~25-30k tokens for most models — leaves room for the
|
|
31
|
+
// question, system prompt, and answer within a 128k context window.
|
|
32
|
+
const MAX_SESSION_CHARS = 100_000;
|
|
33
|
+
|
|
34
|
+
const QUERY_SYSTEM_PROMPT = `Extract information relevant to the question from the session history.
|
|
35
|
+
Return a concise answer using bullet points where appropriate.
|
|
36
|
+
Use code pointers (path/to/file.ts:42 or path/to/file.ts#functionName) when referencing specific code.
|
|
37
|
+
If the information is not in the session, say so clearly.`;
|
|
38
|
+
|
|
39
|
+
export default function (pi: ExtensionAPI) {
|
|
40
|
+
pi.registerTool({
|
|
41
|
+
name: "session_query",
|
|
42
|
+
label: (params) => `Query Session: ${params.question}`,
|
|
43
|
+
description:
|
|
44
|
+
"Query a previous pi session file for context, decisions, or information. Use when you need to look up what happened in a parent session or any other session. The sessionPath should be the full path to a .jsonl session file.",
|
|
45
|
+
|
|
46
|
+
parameters: Type.Object({
|
|
47
|
+
sessionPath: Type.String({
|
|
48
|
+
description:
|
|
49
|
+
"Full path to the session file (e.g., /home/user/.pi/agent/sessions/.../session.jsonl)",
|
|
50
|
+
}),
|
|
51
|
+
question: Type.String({
|
|
52
|
+
description:
|
|
53
|
+
"What you want to know about that session (e.g., 'What files were modified?' or 'What approach was chosen?')",
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
renderResult(result, _options, theme) {
|
|
58
|
+
const container = new Container();
|
|
59
|
+
|
|
60
|
+
if (result.content && result.content[0]?.text) {
|
|
61
|
+
const text = result.content[0].text;
|
|
62
|
+
|
|
63
|
+
// Check for error format
|
|
64
|
+
if (result.details?.error) {
|
|
65
|
+
container.addChild(new Text(theme.fg("error", text), 0, 0));
|
|
66
|
+
return container;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Parse structured response: **Query:** question\n\n---\n\nanswer
|
|
70
|
+
const match = text.match(/\*\*Query:\*\* (.+?)\n\n---\n\n([\s\S]+)/);
|
|
71
|
+
|
|
72
|
+
if (match) {
|
|
73
|
+
const [, query, answer] = match;
|
|
74
|
+
container.addChild(new Text(theme.bold("Query: ") + theme.fg("accent", query), 0, 0));
|
|
75
|
+
container.addChild(new Spacer(1));
|
|
76
|
+
// Render the answer as markdown
|
|
77
|
+
container.addChild(
|
|
78
|
+
new Markdown(answer.trim(), 0, 0, getMarkdownTheme(), {
|
|
79
|
+
color: (text: string) => theme.fg("toolOutput", text),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
// Fallback for other formats
|
|
84
|
+
container.addChild(new Text(theme.fg("toolOutput", text), 0, 0));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show metadata if available
|
|
88
|
+
if (result.details?.messageCount) {
|
|
89
|
+
const truncNote = result.details.truncated ? ", truncated" : "";
|
|
90
|
+
container.addChild(new Spacer(1));
|
|
91
|
+
container.addChild(
|
|
92
|
+
new Text(
|
|
93
|
+
theme.fg("dim", `(${result.details.messageCount} messages in session${truncNote})`),
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return container;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
105
|
+
const { sessionPath, question } = params;
|
|
106
|
+
|
|
107
|
+
// Helper for error returns
|
|
108
|
+
const errorResult = (text: string) => ({
|
|
109
|
+
content: [{ type: "text" as const, text }],
|
|
110
|
+
details: { error: true },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Validate session path
|
|
114
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
115
|
+
return errorResult(
|
|
116
|
+
`Error: Invalid session path. Expected a .jsonl file, got: ${sessionPath}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if file exists
|
|
121
|
+
if (!fs.existsSync(sessionPath)) {
|
|
122
|
+
return errorResult(`Error: Session file not found: ${sessionPath}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onUpdate?.({
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Querying session: ${question}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
details: { status: "loading", question },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Load the session
|
|
136
|
+
let sessionManager: SessionManager;
|
|
137
|
+
try {
|
|
138
|
+
sessionManager = SessionManager.open(sessionPath);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return errorResult(`Error loading session: ${err}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get conversation from the session
|
|
144
|
+
const branch = sessionManager.getBranch();
|
|
145
|
+
const messages = branch
|
|
146
|
+
.filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
|
|
147
|
+
.map((entry) => entry.message);
|
|
148
|
+
|
|
149
|
+
if (messages.length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text" as const, text: "Session is empty - no messages found." }],
|
|
152
|
+
details: { empty: true },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Serialize the conversation, truncating if too large
|
|
157
|
+
const llmMessages = convertToLlm(messages);
|
|
158
|
+
let conversationText = serializeConversation(llmMessages);
|
|
159
|
+
let truncated = false;
|
|
160
|
+
|
|
161
|
+
if (conversationText.length > MAX_SESSION_CHARS) {
|
|
162
|
+
// Keep the tail (most recent context) — more likely to be relevant
|
|
163
|
+
conversationText = "…[earlier messages truncated]…\n\n"
|
|
164
|
+
+ conversationText.slice(-MAX_SESSION_CHARS);
|
|
165
|
+
truncated = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Use LLM to answer the question
|
|
169
|
+
if (!ctx.model) {
|
|
170
|
+
return errorResult("Error: No model available to analyze the session.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
|
|
175
|
+
|
|
176
|
+
const userMessage: Message = {
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: "text",
|
|
181
|
+
text: `## Session Conversation\n\n${conversationText}\n\n## Question\n\n${question}`,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const response = await complete(
|
|
188
|
+
ctx.model,
|
|
189
|
+
{ systemPrompt: QUERY_SYSTEM_PROMPT, messages: [userMessage] },
|
|
190
|
+
{ apiKey, signal },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (response.stopReason === "aborted") {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text" as const, text: "Query was cancelled." }],
|
|
196
|
+
details: { cancelled: true },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const answer = response.content
|
|
201
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
202
|
+
.map((c) => c.text)
|
|
203
|
+
.join("\n");
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text" as const, text: `**Query:** ${question}\n\n---\n\n${answer}` }],
|
|
207
|
+
details: {
|
|
208
|
+
sessionPath,
|
|
209
|
+
question,
|
|
210
|
+
messageCount: messages.length,
|
|
211
|
+
truncated,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return errorResult(`Error querying session: ${err}`);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ssweens/pi-compaxxt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enhanced compaction for pi — session context and LLM-ranked important files",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "ssweens",
|
|
11
|
+
"files": [
|
|
12
|
+
"extensions/",
|
|
13
|
+
"skills/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"extensions"
|
|
20
|
+
],
|
|
21
|
+
"skills": [
|
|
22
|
+
"skills"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@mariozechner/pi-ai": "*",
|
|
27
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
28
|
+
"@mariozechner/pi-tui": "*",
|
|
29
|
+
"@sinclair/typebox": "*"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-session-query
|
|
3
|
+
description: Query previous pi sessions to retrieve context, decisions, code changes, or other information. Use when you need to look up what happened in a parent session or any other session file.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Pi Session Query
|
|
8
|
+
|
|
9
|
+
Query pi session files to retrieve context from past conversations.
|
|
10
|
+
|
|
11
|
+
This skill is automatically invoked in handed-off sessions when you need to look up details from the parent session.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- When the handoff summary references a "Parent session" or "Ancestor sessions" path
|
|
16
|
+
- When you need specific details not included in the handoff summary
|
|
17
|
+
- When you need to verify a decision or approach from the parent or an ancestor session
|
|
18
|
+
- When you need file paths or code snippets from earlier work
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Use the `session_query` tool:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
session_query(sessionPath, question)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Parameters:**
|
|
29
|
+
- `sessionPath`: Full path to the session file (provided in the "Parent session:" line)
|
|
30
|
+
- `question`: Specific question about that session
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Find what files were changed
|
|
36
|
+
session_query("/path/to/session.jsonl", "What files were modified?")
|
|
37
|
+
|
|
38
|
+
// Get approach details
|
|
39
|
+
session_query("/path/to/session.jsonl", "What approach was chosen for authentication?")
|
|
40
|
+
|
|
41
|
+
// Get specific code decisions
|
|
42
|
+
session_query("/path/to/session.jsonl", "What error handling pattern was used?")
|
|
43
|
+
|
|
44
|
+
// Summarize key decisions
|
|
45
|
+
session_query("/path/to/session.jsonl", "Summarize the key decisions made")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Best Practices
|
|
49
|
+
|
|
50
|
+
1. **Be specific** - Ask targeted questions for better results
|
|
51
|
+
2. **Reference code** - Ask about specific files or functions when relevant
|
|
52
|
+
3. **Verify before assuming** - If the handoff summary seems incomplete, query for details
|
|
53
|
+
4. **Don't over-query** - The handoff summary should have most context; query only when needed
|
|
54
|
+
5. **Check ancestors** - If the parent session doesn't have the info, try ancestor sessions listed in the handoff
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
The tool loads the referenced session file, extracts the conversation history, and uses the LLM to answer your question based on its contents. This allows context retrieval without loading the full parent session into your context window.
|