@pi-unipi/compactor 0.2.3 → 2.0.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 +3 -1
- package/package.json +2 -2
- package/src/commands/index.ts +79 -169
- package/src/compaction/content.ts +2 -2
- package/src/compaction/cut.ts +10 -6
- package/src/compaction/hooks.ts +82 -52
- package/src/compaction/recall-scope.ts +1 -1
- package/src/config/manager.ts +0 -0
- package/src/config/presets.ts +10 -10
- package/src/executor/executor.ts +4 -4
- package/src/index.ts +34 -45
- package/src/info-screen.ts +97 -40
- package/src/session/db.ts +40 -11
- package/src/session/extract.ts +37 -0
- package/src/tools/ctx-batch-execute.ts +5 -16
- package/src/tools/ctx-doctor.ts +0 -18
- package/src/tools/ctx-stats.ts +43 -10
- package/src/tools/register.ts +30 -122
- package/src/tui/settings-overlay.ts +12 -21
- package/src/types.ts +8 -26
- package/src/store/chunking.ts +0 -126
- package/src/store/db-base.ts +0 -87
- package/src/store/index.ts +0 -513
- package/src/store/unified.ts +0 -109
- package/src/tools/ctx-fetch-and-index.ts +0 -32
- package/src/tools/ctx-index.ts +0 -36
- package/src/tools/ctx-search.ts +0 -19
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The zero-LLM pipeline compresses context through 6 stages (normalize, filter, bu
|
|
|
8
8
|
|
|
9
9
|
| Command | Description |
|
|
10
10
|
|---------|-------------|
|
|
11
|
-
| `/unipi:compact` |
|
|
11
|
+
| `/unipi:lossless-compact` | Immediate zero-LLM compaction with structured summary |
|
|
12
12
|
| `/unipi:session-recall` | Search session history (BM25 or regex) |
|
|
13
13
|
| `/unipi:content-index` | Index current project into FTS5 |
|
|
14
14
|
| `/unipi:content-search` | Search indexed content |
|
|
@@ -19,6 +19,8 @@ The zero-LLM pipeline compresses context through 6 stages (normalize, filter, bu
|
|
|
19
19
|
| `/unipi:compact-preset <name>` | Apply quick preset |
|
|
20
20
|
| `/unipi:compact-help` | Show detailed documentation |
|
|
21
21
|
|
|
22
|
+
> **Note:** `/unipi:compact` still works as a deprecated alias for `/unipi:lossless-compact`.
|
|
23
|
+
|
|
22
24
|
## Special Triggers
|
|
23
25
|
|
|
24
26
|
Compactor tools are available to the main agent when installed. All workflow skills can use compactor tools for context management.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution,
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Neuron Mr White",
|
package/src/commands/index.ts
CHANGED
|
@@ -2,33 +2,22 @@
|
|
|
2
2
|
* All /unipi:compact-* commands
|
|
3
3
|
*
|
|
4
4
|
* Commands perform real work by calling tool implementations directly.
|
|
5
|
-
* Dependencies (sessionDB,
|
|
6
|
-
*
|
|
7
|
-
* New command names (v0.2.0):
|
|
8
|
-
* /unipi:session-recall (was /unipi:compact-recall)
|
|
9
|
-
* /unipi:content-index (was /unipi:compact-index)
|
|
10
|
-
* /unipi:content-search (was /unipi:compact-search)
|
|
11
|
-
* /unipi:content-purge (was /unipi:compact-purge)
|
|
12
|
-
*
|
|
13
|
-
* Old names kept as deprecated aliases for backward compatibility.
|
|
5
|
+
* Dependencies (sessionDB, sessionId) are injected at registration time.
|
|
14
6
|
*/
|
|
15
7
|
|
|
16
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
17
9
|
import { loadConfig, saveConfig } from "../config/manager.js";
|
|
18
10
|
import { applyPreset, parsePreset } from "../config/presets.js";
|
|
11
|
+
import { COMPACTOR_INSTRUCTION } from "@pi-unipi/core";
|
|
19
12
|
import { getLastCompactionStats } from "../compaction/hooks.js";
|
|
20
|
-
import { compactTool } from "../tools/compact.js";
|
|
21
13
|
import { vccRecall } from "../tools/vcc-recall.js";
|
|
22
14
|
import { ctxStats } from "../tools/ctx-stats.js";
|
|
23
15
|
import { ctxDoctor } from "../tools/ctx-doctor.js";
|
|
24
|
-
import { ctxSearch } from "../tools/ctx-search.js";
|
|
25
|
-
import { ContentStore } from "../store/index.js";
|
|
26
16
|
import type { SessionDB } from "../session/db.js";
|
|
27
17
|
import type { NormalizedBlock, RuntimeCounters } from "../types.js";
|
|
28
18
|
|
|
29
19
|
export interface CommandDeps {
|
|
30
20
|
sessionDB: SessionDB | null;
|
|
31
|
-
contentStore: ContentStore | null;
|
|
32
21
|
getSessionId: () => string;
|
|
33
22
|
getBlocks: () => NormalizedBlock[];
|
|
34
23
|
getCounters?: () => RuntimeCounters;
|
|
@@ -38,22 +27,70 @@ function deprecationLog(_oldName: string, _newName: string): void {
|
|
|
38
27
|
// Deprecation logging disabled — was writing to stdout causing TUI rendering issues.
|
|
39
28
|
}
|
|
40
29
|
|
|
30
|
+
const formatTokens = (n: number): string => {
|
|
31
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
32
|
+
return String(n);
|
|
33
|
+
};
|
|
34
|
+
|
|
41
35
|
export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
42
|
-
// ── /unipi:compact
|
|
36
|
+
// ── /unipi:lossless-compact ──────────────────────────
|
|
37
|
+
pi.registerCommand("unipi:lossless-compact", {
|
|
38
|
+
description: "Immediate zero-LLM compaction — structured summary with full recall",
|
|
39
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
40
|
+
ctx.compact({
|
|
41
|
+
customInstructions: COMPACTOR_INSTRUCTION,
|
|
42
|
+
onComplete: () => {
|
|
43
|
+
const stats = getLastCompactionStats();
|
|
44
|
+
if (stats) {
|
|
45
|
+
ctx.ui.notify(
|
|
46
|
+
`Compacted ${stats.totalMessages} messages (~${formatTokens(stats.tokensBefore)} tokens) → ${stats.kept} messages (~${formatTokens(stats.tokensAfterEst)} tokens)`,
|
|
47
|
+
"info",
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
ctx.ui.notify("Compaction completed.", "info");
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onError: (err: Error) => {
|
|
54
|
+
if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
|
|
55
|
+
ctx.ui.notify("Nothing to compact.", "info");
|
|
56
|
+
} else {
|
|
57
|
+
ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Deprecated alias — old name
|
|
43
64
|
pi.registerCommand("unipi:compact", {
|
|
44
|
-
description: "
|
|
45
|
-
handler: async (_args: string, ctx:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
description: "(DEPRECATED) Use /unipi:lossless-compact instead",
|
|
66
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
67
|
+
deprecationLog("/unipi:compact", "/unipi:lossless-compact");
|
|
68
|
+
ctx.compact({
|
|
69
|
+
customInstructions: COMPACTOR_INSTRUCTION,
|
|
70
|
+
onComplete: () => {
|
|
71
|
+
const stats = getLastCompactionStats();
|
|
72
|
+
if (stats) {
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
`Compacted ${stats.totalMessages} messages (~${formatTokens(stats.tokensBefore)} tokens) → ${stats.kept} messages (~${formatTokens(stats.tokensAfterEst)} tokens)`,
|
|
75
|
+
"info",
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
ctx.ui.notify("Compaction completed.", "info");
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onError: (err: Error) => {
|
|
82
|
+
if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
|
|
83
|
+
ctx.ui.notify("Nothing to compact.", "info");
|
|
84
|
+
} else {
|
|
85
|
+
ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
52
89
|
},
|
|
53
90
|
});
|
|
54
91
|
|
|
55
92
|
// ── /unipi:session-recall (new) ─────────────────────
|
|
56
|
-
const sessionRecallHandler = async (args: string, ctx:
|
|
93
|
+
const sessionRecallHandler = async (args: string, ctx: ExtensionCommandContext) => {
|
|
57
94
|
const query = args.trim();
|
|
58
95
|
if (!query) {
|
|
59
96
|
ctx.ui.notify("Usage: /unipi:session-recall <query>", "warning");
|
|
@@ -81,7 +118,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
81
118
|
// Deprecated alias
|
|
82
119
|
pi.registerCommand("unipi:compact-recall", {
|
|
83
120
|
description: "(DEPRECATED) Search session history — use /unipi:session-recall instead",
|
|
84
|
-
handler: async (args: string, ctx:
|
|
121
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
85
122
|
deprecationLog("/unipi:compact-recall", "/unipi:session-recall");
|
|
86
123
|
return sessionRecallHandler(args, ctx);
|
|
87
124
|
},
|
|
@@ -90,19 +127,18 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
90
127
|
// ── /unipi:compact-stats ─────────────────────────────
|
|
91
128
|
pi.registerCommand("unipi:compact-stats", {
|
|
92
129
|
description: "Show context savings dashboard",
|
|
93
|
-
handler: async (_args: string, ctx:
|
|
94
|
-
if (!deps?.sessionDB
|
|
130
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
131
|
+
if (!deps?.sessionDB) {
|
|
95
132
|
ctx.ui.notify("Compactor services not initialized.", "error");
|
|
96
133
|
return;
|
|
97
134
|
}
|
|
98
135
|
try {
|
|
99
|
-
const stats = await ctxStats(deps.sessionDB, deps.
|
|
136
|
+
const stats = await ctxStats(deps.sessionDB, deps.getSessionId(), deps.getCounters?.());
|
|
100
137
|
const lines = [
|
|
101
138
|
"📊 Compactor Stats",
|
|
102
139
|
`Session events: ${stats.sessionEvents}`,
|
|
103
140
|
`Compactions: ${stats.compactions}`,
|
|
104
141
|
`Tokens saved: ${stats.tokensSaved}`,
|
|
105
|
-
`Indexed docs: ${stats.indexedDocs} (${stats.indexedChunks} chunks)`,
|
|
106
142
|
`Sandbox runs: ${stats.sandboxRuns}`,
|
|
107
143
|
`Search queries: ${stats.searchQueries}`,
|
|
108
144
|
];
|
|
@@ -116,13 +152,13 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
116
152
|
// ── /unipi:compact-doctor ────────────────────────────
|
|
117
153
|
pi.registerCommand("unipi:compact-doctor", {
|
|
118
154
|
description: "Run diagnostics checklist",
|
|
119
|
-
handler: async (_args: string, ctx:
|
|
120
|
-
if (!deps?.sessionDB
|
|
155
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
156
|
+
if (!deps?.sessionDB) {
|
|
121
157
|
ctx.ui.notify("Compactor services not initialized.", "error");
|
|
122
158
|
return;
|
|
123
159
|
}
|
|
124
160
|
try {
|
|
125
|
-
const result = await ctxDoctor(deps.sessionDB
|
|
161
|
+
const result = await ctxDoctor(deps.sessionDB);
|
|
126
162
|
const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
|
|
127
163
|
const lines = [
|
|
128
164
|
result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
|
|
@@ -139,7 +175,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
139
175
|
// ── /unipi:compact-settings ──────────────────────────
|
|
140
176
|
pi.registerCommand("unipi:compact-settings", {
|
|
141
177
|
description: "Open TUI settings overlay",
|
|
142
|
-
handler: async (_args: string, ctx:
|
|
178
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
143
179
|
try {
|
|
144
180
|
const cwd = (ctx as any).cwd ?? process.cwd();
|
|
145
181
|
const { renderSettingsOverlay } = await import("../tui/settings-overlay.js");
|
|
@@ -158,7 +194,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
158
194
|
// ── /unipi:compact-preset ────────────────────────────
|
|
159
195
|
pi.registerCommand("unipi:compact-preset", {
|
|
160
196
|
description: "Apply quick preset (precise/balanced/thorough/lean)",
|
|
161
|
-
handler: async (args: string, ctx:
|
|
197
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
162
198
|
const presetName = parsePreset(args.trim());
|
|
163
199
|
if (!presetName) {
|
|
164
200
|
ctx.ui.notify("Unknown preset. Use: precise, balanced, thorough, lean", "error");
|
|
@@ -174,150 +210,24 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
174
210
|
},
|
|
175
211
|
});
|
|
176
212
|
|
|
177
|
-
// ── /unipi:content-index (new) / /unipi:compact-index (deprecated) ──
|
|
178
|
-
const contentIndexHandler = async (_args: string, ctx: any) => {
|
|
179
|
-
if (!deps?.contentStore) {
|
|
180
|
-
ctx.ui.notify("Content store not initialized. Enable fts5Index in config.", "warning");
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const cwd = (ctx as any).cwd ?? process.cwd();
|
|
185
|
-
const { readdirSync, readFileSync, statSync } = await import("node:fs");
|
|
186
|
-
const { join, relative, extname } = await import("node:path");
|
|
187
|
-
|
|
188
|
-
const indexable = [".md", ".txt", ".ts", ".js", ".json", ".py", ".sh"];
|
|
189
|
-
const files: string[] = [];
|
|
190
|
-
|
|
191
|
-
const walk = (dir: string, depth = 0) => {
|
|
192
|
-
if (depth > 3) return;
|
|
193
|
-
try {
|
|
194
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
195
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
196
|
-
const full = join(dir, entry.name);
|
|
197
|
-
if (entry.isDirectory()) {
|
|
198
|
-
walk(full, depth + 1);
|
|
199
|
-
} else if (indexable.includes(extname(entry.name))) {
|
|
200
|
-
files.push(full);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
// skip unreadable dirs
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
walk(cwd);
|
|
209
|
-
let totalChunks = 0;
|
|
210
|
-
for (const file of files.slice(0, 100)) {
|
|
211
|
-
try {
|
|
212
|
-
const content = readFileSync(file, "utf-8");
|
|
213
|
-
if (content.length < 50) continue;
|
|
214
|
-
const ext = extname(file);
|
|
215
|
-
const ct = ext === ".md" ? "markdown" : ext === ".json" ? "json" : "plain";
|
|
216
|
-
const result = await deps.contentStore!.index(relative(cwd, file), content, {
|
|
217
|
-
contentType: ct,
|
|
218
|
-
source: file,
|
|
219
|
-
});
|
|
220
|
-
totalChunks += result.totalChunks;
|
|
221
|
-
} catch {
|
|
222
|
-
// skip unreadable files
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
ctx.ui.notify(`Indexed ${Math.min(files.length, 100)} files (${totalChunks} chunks).`, "info");
|
|
226
|
-
} catch (err) {
|
|
227
|
-
ctx.ui.notify(`Index error: ${err}`, "error");
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
pi.registerCommand("unipi:content-index", {
|
|
231
|
-
description: "Index current project files into FTS5",
|
|
232
|
-
handler: contentIndexHandler,
|
|
233
|
-
});
|
|
234
|
-
pi.registerCommand("unipi:compact-index", {
|
|
235
|
-
description: "(DEPRECATED) Index project files — use /unipi:content-index instead",
|
|
236
|
-
handler: async (args: string, ctx: any) => {
|
|
237
|
-
deprecationLog("/unipi:compact-index", "/unipi:content-index");
|
|
238
|
-
return contentIndexHandler(args, ctx);
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// ── /unipi:content-search (new) / /unipi:compact-search (deprecated) ──
|
|
243
|
-
const contentSearchHandler = async (args: string, ctx: any) => {
|
|
244
|
-
const query = args.trim();
|
|
245
|
-
if (!query) {
|
|
246
|
-
ctx.ui.notify("Usage: /unipi:content-search <query>", "warning");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (!deps?.contentStore) {
|
|
250
|
-
ctx.ui.notify("Content store not initialized.", "warning");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
const results = await ctxSearch(deps.contentStore!, { query, limit: 10 });
|
|
255
|
-
if (results.length === 0) {
|
|
256
|
-
ctx.ui.notify(`No results for "${query}".`, "info");
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const lines = results.map(
|
|
260
|
-
(r, i) => `[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 200)}`,
|
|
261
|
-
);
|
|
262
|
-
ctx.ui.notify(`Found ${results.length} results:\n${lines.join("\n\n")}`, "info");
|
|
263
|
-
} catch (err) {
|
|
264
|
-
ctx.ui.notify(`Search error: ${err}`, "error");
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
pi.registerCommand("unipi:content-search", {
|
|
268
|
-
description: "Search indexed content",
|
|
269
|
-
handler: contentSearchHandler,
|
|
270
|
-
});
|
|
271
|
-
pi.registerCommand("unipi:compact-search", {
|
|
272
|
-
description: "(DEPRECATED) Search indexed content — use /unipi:content-search instead",
|
|
273
|
-
handler: async (args: string, ctx: any) => {
|
|
274
|
-
deprecationLog("/unipi:compact-search", "/unipi:content-search");
|
|
275
|
-
return contentSearchHandler(args, ctx);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// ── /unipi:content-purge (new) / /unipi:compact-purge (deprecated) ──
|
|
280
|
-
const contentPurgeHandler = async (_args: string, ctx: any) => {
|
|
281
|
-
if (!deps?.contentStore) {
|
|
282
|
-
ctx.ui.notify("Content store not initialized.", "warning");
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
await deps.contentStore!.purge();
|
|
287
|
-
ctx.ui.notify("All indexed content purged.", "info");
|
|
288
|
-
} catch (err) {
|
|
289
|
-
ctx.ui.notify(`Purge error: ${err}`, "error");
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
pi.registerCommand("unipi:content-purge", {
|
|
293
|
-
description: "Wipe all indexed content from FTS5",
|
|
294
|
-
handler: contentPurgeHandler,
|
|
295
|
-
});
|
|
296
|
-
pi.registerCommand("unipi:compact-purge", {
|
|
297
|
-
description: "(DEPRECATED) Wipe indexed content — use /unipi:content-purge instead",
|
|
298
|
-
handler: async (args: string, ctx: any) => {
|
|
299
|
-
deprecationLog("/unipi:compact-purge", "/unipi:content-purge");
|
|
300
|
-
return contentPurgeHandler(args, ctx);
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
|
|
304
213
|
// ── /unipi:compact-help ──────────────────────────────
|
|
305
214
|
pi.registerCommand("unipi:compact-help", {
|
|
306
215
|
description: "Show detailed compactor documentation (tier-2 skill)",
|
|
307
|
-
handler: async (_args: string, ctx:
|
|
308
|
-
// Load tier-2 skill content — delegates to skill loading system
|
|
216
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
309
217
|
ctx.ui.notify(
|
|
310
|
-
"🗜️ Compactor Help
|
|
218
|
+
"🗜️ Compactor Help\n" +
|
|
311
219
|
"Quick commands:\n" +
|
|
312
|
-
" /unipi:compact — trigger compaction\n" +
|
|
220
|
+
" /unipi:lossless-compact — trigger immediate compaction\n" +
|
|
313
221
|
" /unipi:session-recall <query> — search session history\n" +
|
|
314
|
-
" /unipi:content-index — index project files\n" +
|
|
315
|
-
" /unipi:content-search <query> — search indexed content\n" +
|
|
316
|
-
" /unipi:content-purge — wipe indexed content\n" +
|
|
317
222
|
" /unipi:compact-stats — view stats\n" +
|
|
318
223
|
" /unipi:compact-doctor — run diagnostics\n" +
|
|
319
224
|
" /unipi:compact-settings — TUI settings\n" +
|
|
320
|
-
" /unipi:compact-preset <name> — apply preset"
|
|
225
|
+
" /unipi:compact-preset <name> — apply preset\n" +
|
|
226
|
+
"\n" +
|
|
227
|
+
"Content indexing has moved to @pi-unipi/cocoindex:\n" +
|
|
228
|
+
" /unipi:cocoindex-init — initialize pipeline\n" +
|
|
229
|
+
" /unipi:cocoindex-update — index project files\n" +
|
|
230
|
+
" cocoindex_search — search indexed content",
|
|
321
231
|
"info",
|
|
322
232
|
);
|
|
323
233
|
},
|
|
@@ -7,8 +7,8 @@ export function textOf(content: unknown): string {
|
|
|
7
7
|
if (typeof content === "string") return content;
|
|
8
8
|
if (Array.isArray(content)) {
|
|
9
9
|
return content
|
|
10
|
-
.map((c:
|
|
11
|
-
if (c?.type === "text") return c.text ?? "";
|
|
10
|
+
.map((c: Record<string, unknown>) => {
|
|
11
|
+
if (c?.type === "text") return (c.text as string) ?? "";
|
|
12
12
|
if (c?.type === "toolCall") return `[toolCall:${c.name}]`;
|
|
13
13
|
if (c?.type === "thinking") return "[thinking]";
|
|
14
14
|
if (c?.type === "image") return `[image:${c.mimeType}]`;
|
package/src/compaction/cut.ts
CHANGED
|
@@ -7,28 +7,32 @@ export type OwnCutCancelReason =
|
|
|
7
7
|
| "too_few_live_messages"
|
|
8
8
|
| "no_user_message";
|
|
9
9
|
|
|
10
|
+
import type { SessionEntry, SessionMessageEntry, CompactionEntry } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
12
|
+
|
|
10
13
|
export type OwnCutResult =
|
|
11
|
-
| { ok: true; messages:
|
|
14
|
+
| { ok: true; messages: AgentMessage[]; firstKeptEntryId: string; compactAll: boolean }
|
|
12
15
|
| { ok: false; reason: OwnCutCancelReason };
|
|
13
16
|
|
|
14
17
|
interface EntryWithMessage {
|
|
15
|
-
entry:
|
|
16
|
-
message:
|
|
18
|
+
entry: SessionEntry;
|
|
19
|
+
message: AgentMessage;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export function buildOwnCut(branchEntries:
|
|
22
|
+
export function buildOwnCut(branchEntries: SessionEntry[]): OwnCutResult {
|
|
20
23
|
let lastCompactionIdx = -1;
|
|
21
24
|
let lastKeptId: string | undefined;
|
|
22
25
|
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
23
26
|
if (branchEntries[i].type === "compaction") {
|
|
24
27
|
lastCompactionIdx = i;
|
|
25
|
-
|
|
28
|
+
const ce = branchEntries[i] as CompactionEntry;
|
|
29
|
+
lastKeptId = ce.firstKeptEntryId;
|
|
26
30
|
break;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
const hasPriorCompaction = lastCompactionIdx >= 0;
|
|
31
|
-
const hasValidKeptId = !!lastKeptId && branchEntries.some((e
|
|
35
|
+
const hasValidKeptId = !!lastKeptId && branchEntries.some((e) => e.id === lastKeptId);
|
|
32
36
|
const orphanRecovery = hasPriorCompaction && !hasValidKeptId;
|
|
33
37
|
|
|
34
38
|
const liveMessages: EntryWithMessage[] = [];
|
package/src/compaction/hooks.ts
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { convertToLlm } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import type {
|
|
8
|
+
SessionEntry,
|
|
9
|
+
SessionMessageEntry,
|
|
10
|
+
SessionBeforeCompactEvent,
|
|
11
|
+
SessionCompactEvent,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
7
14
|
import { compile } from "./summarize.js";
|
|
8
15
|
import { loadConfig } from "../config/manager.js";
|
|
9
|
-
import { buildOwnCut } from "./cut.js";
|
|
16
|
+
import { buildOwnCut, type OwnCutResult } from "./cut.js";
|
|
10
17
|
import type { CompactionStats } from "../types.js";
|
|
11
18
|
import type { SessionDB } from "../session/db.js";
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
import { COMPACTOR_INSTRUCTION } from "@pi-unipi/core";
|
|
14
21
|
|
|
15
22
|
let lastStats: CompactionStats | null = null;
|
|
16
23
|
let lastCompactWasCompactor = false;
|
|
@@ -26,76 +33,105 @@ const dbg = (_debug: boolean, _event: string, _data?: Record<string, unknown>) =
|
|
|
26
33
|
return;
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
const previewContent = (content: unknown): string => {
|
|
30
|
-
if (typeof content === "string") return content.slice(0, 300);
|
|
31
|
-
if (Array.isArray(content)) {
|
|
32
|
-
return content
|
|
33
|
-
.map((c: any) => {
|
|
34
|
-
if (c?.type === "text") return c.text ?? "";
|
|
35
|
-
if (c?.type === "toolCall") return `[toolCall:${c.name}]`;
|
|
36
|
-
if (c?.type === "thinking") return `[thinking]`;
|
|
37
|
-
if (c?.type === "image") return `[image:${c.mimeType}]`;
|
|
38
|
-
return `[${c?.type ?? "unknown"}]`;
|
|
39
|
-
})
|
|
40
|
-
.join("\n")
|
|
41
|
-
.slice(0, 300);
|
|
42
|
-
}
|
|
43
|
-
return "";
|
|
44
|
-
};
|
|
45
|
-
|
|
46
36
|
const REASON_MESSAGES: Record<import("./cut.js").OwnCutCancelReason, string> = {
|
|
47
37
|
no_live_messages: "compactor: Nothing to compact (no live messages)",
|
|
48
38
|
too_few_live_messages: "compactor: Too few messages to compact",
|
|
49
39
|
no_user_message: "compactor: Cannot compact — no user message found",
|
|
50
40
|
};
|
|
51
41
|
|
|
42
|
+
/** Count chars in a content part array (TextContent, ToolCall, ToolResult, etc.) */
|
|
43
|
+
function contentPartsChars(parts: Array<{ text?: string; name?: string; input?: unknown; content?: unknown }>): number {
|
|
44
|
+
return parts.reduce((s: number, p) => {
|
|
45
|
+
if (p.text) return s + p.text.length;
|
|
46
|
+
if (p.name) {
|
|
47
|
+
// ToolCall
|
|
48
|
+
const inputStr = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? "");
|
|
49
|
+
return s + p.name.length + inputStr.length;
|
|
50
|
+
}
|
|
51
|
+
if (p.content !== undefined) {
|
|
52
|
+
// ToolResult
|
|
53
|
+
const contentStr = typeof p.content === "string" ? p.content : JSON.stringify(p.content ?? "");
|
|
54
|
+
return s + contentStr.length;
|
|
55
|
+
}
|
|
56
|
+
return s;
|
|
57
|
+
}, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Estimate char count for an AgentMessage (unwrapped — has role + content directly) */
|
|
61
|
+
function messageChars(msg: AgentMessage): number {
|
|
62
|
+
const c = (msg as { content: unknown }).content;
|
|
63
|
+
if (typeof c === "string") return c.length;
|
|
64
|
+
if (Array.isArray(c)) return contentPartsChars(c as Array<{ text?: string; name?: string; input?: unknown; content?: unknown }>);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Estimate char count for a SessionMessageEntry's message */
|
|
69
|
+
function entryMessageChars(entry: SessionMessageEntry): number {
|
|
70
|
+
return messageChars(entry.message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Filter entries to only SessionMessageEntry */
|
|
74
|
+
function filterMessageEntries(entries: SessionEntry[]): SessionMessageEntry[] {
|
|
75
|
+
return entries.filter((e): e is SessionMessageEntry => e.type === "message");
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
export function registerCompactionHooks(
|
|
53
79
|
pi: ExtensionAPI,
|
|
54
80
|
deps?: { getSessionDB?: () => SessionDB | null; getSessionId?: () => string },
|
|
55
81
|
): void {
|
|
56
|
-
pi.on("session_before_compact", (event, ctx) => {
|
|
82
|
+
pi.on("session_before_compact", (event: SessionBeforeCompactEvent, ctx) => {
|
|
57
83
|
const { preparation, branchEntries, customInstructions } = event;
|
|
58
84
|
const config = loadConfig();
|
|
59
|
-
|
|
85
|
+
const isCompactor = customInstructions?.startsWith(COMPACTOR_INSTRUCTION) ?? false;
|
|
86
|
+
dbg(config.debug, "session_before_compact:enter", {
|
|
87
|
+
entryCount: branchEntries.length,
|
|
88
|
+
hasPrevSummary: !!preparation?.previousSummary,
|
|
89
|
+
isCompactor,
|
|
90
|
+
});
|
|
60
91
|
|
|
61
|
-
const isCompactor = customInstructions === COMPACTOR_INSTRUCTION;
|
|
62
92
|
if (!isCompactor && !config.overrideDefaultCompaction) {
|
|
63
93
|
dbg(config.debug, "session_before_compact:skip", { reason: "not_compactor_and_no_override" });
|
|
64
94
|
return;
|
|
65
95
|
}
|
|
66
96
|
|
|
67
|
-
const ownCut = buildOwnCut(branchEntries
|
|
68
|
-
dbg(config.debug, "buildOwnCut", {
|
|
97
|
+
const ownCut: OwnCutResult = buildOwnCut(branchEntries);
|
|
98
|
+
dbg(config.debug, "buildOwnCut", {
|
|
99
|
+
ok: ownCut.ok,
|
|
100
|
+
reason: !ownCut.ok ? (ownCut as { ok: false; reason: string }).reason : undefined,
|
|
101
|
+
});
|
|
69
102
|
if (!ownCut.ok) {
|
|
70
103
|
try {
|
|
71
|
-
ctx?.ui?.notify?.(REASON_MESSAGES[ownCut.reason], "warning");
|
|
104
|
+
ctx?.ui?.notify?.(REASON_MESSAGES[(ownCut as { ok: false; reason: import("./cut.js").OwnCutCancelReason }).reason], "warning");
|
|
72
105
|
} catch {}
|
|
73
106
|
return { cancel: true };
|
|
74
107
|
}
|
|
75
108
|
|
|
76
|
-
const agentMessages = ownCut
|
|
77
|
-
const firstKeptEntryId = ownCut.firstKeptEntryId;
|
|
109
|
+
const { messages: agentMessages, firstKeptEntryId } = ownCut;
|
|
78
110
|
const messages = convertToLlm(agentMessages);
|
|
79
111
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
112
|
+
// Find kept entries (from cut point onward)
|
|
113
|
+
const keptIdx = branchEntries.findIndex((e: SessionEntry) => e.id === firstKeptEntryId);
|
|
114
|
+
const keptMessageEntries: SessionMessageEntry[] = keptIdx >= 0
|
|
115
|
+
? filterMessageEntries(branchEntries.slice(keptIdx))
|
|
83
116
|
: [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
|
|
118
|
+
// Compute char estimates for proportional token estimation
|
|
119
|
+
const summarizedChars = agentMessages.reduce((sum, msg) => sum + messageChars(msg), 0);
|
|
120
|
+
const keptChars = keptMessageEntries.reduce((sum, e) => sum + entryMessageChars(e), 0);
|
|
121
|
+
const totalChars = summarizedChars + keptChars;
|
|
122
|
+
|
|
123
|
+
// Use Pi's real token count for "before", estimate "after" proportionally
|
|
124
|
+
const tokensBefore = preparation.tokensBefore;
|
|
125
|
+
const tokensAfterEst = totalChars > 0
|
|
126
|
+
? Math.round(tokensBefore * keptChars / totalChars)
|
|
127
|
+
: 0;
|
|
128
|
+
|
|
95
129
|
lastStats = {
|
|
96
130
|
summarized: agentMessages.length,
|
|
97
|
-
kept:
|
|
98
|
-
|
|
131
|
+
kept: keptMessageEntries.length,
|
|
132
|
+
totalMessages: agentMessages.length + keptMessageEntries.length,
|
|
133
|
+
tokensBefore,
|
|
134
|
+
tokensAfterEst,
|
|
99
135
|
};
|
|
100
136
|
|
|
101
137
|
// Persist cumulative compaction stats
|
|
@@ -103,13 +139,7 @@ export function registerCompactionHooks(
|
|
|
103
139
|
if (sessionDB && deps?.getSessionId) {
|
|
104
140
|
try {
|
|
105
141
|
const sessionId = deps.getSessionId();
|
|
106
|
-
|
|
107
|
-
const c = msg.message?.content;
|
|
108
|
-
if (typeof c === "string") return sum + c.length;
|
|
109
|
-
if (Array.isArray(c)) return sum + c.reduce((s: number, p: any) => s + (p.text?.length ?? 0), 0);
|
|
110
|
-
return sum;
|
|
111
|
-
}, 0);
|
|
112
|
-
sessionDB.addCompactionStats(sessionId, charsBefore, keptChars, agentMessages.length);
|
|
142
|
+
sessionDB.addCompactionStats(sessionId, summarizedChars, keptChars, agentMessages.length);
|
|
113
143
|
} catch {
|
|
114
144
|
// non-fatal
|
|
115
145
|
}
|
|
@@ -154,7 +184,7 @@ export function registerCompactionHooks(
|
|
|
154
184
|
};
|
|
155
185
|
});
|
|
156
186
|
|
|
157
|
-
pi.on("session_compact", (event, ctx) => {
|
|
187
|
+
pi.on("session_compact", (event: SessionCompactEvent, ctx) => {
|
|
158
188
|
const config = loadConfig();
|
|
159
189
|
dbg(config.debug, "session_compact", { fromExtension: event.fromExtension, lastCompactWasCompactor });
|
|
160
190
|
if (!event.fromExtension) return;
|
|
@@ -164,7 +194,7 @@ export function registerCompactionHooks(
|
|
|
164
194
|
setTimeout(() => {
|
|
165
195
|
try {
|
|
166
196
|
ctx?.ui?.notify?.(
|
|
167
|
-
`
|
|
197
|
+
`Compacted ${stats.totalMessages} messages (~${formatTokens(stats.tokensBefore)} tokens) → ${stats.kept} messages (~${formatTokens(stats.tokensAfterEst)} tokens)`,
|
|
168
198
|
"info",
|
|
169
199
|
);
|
|
170
200
|
} catch {}
|
|
@@ -12,7 +12,7 @@ export interface LineageRange {
|
|
|
12
12
|
* the most recent compaction boundary.
|
|
13
13
|
*/
|
|
14
14
|
export function getRecallScope(
|
|
15
|
-
branchEntries:
|
|
15
|
+
branchEntries: Array<{ type: string; [key: string]: unknown }>,
|
|
16
16
|
opts?: { expand?: boolean },
|
|
17
17
|
): LineageRange {
|
|
18
18
|
let lastCompactionIdx = -1;
|
package/src/config/manager.ts
CHANGED
|
File without changes
|