@pi-unipi/compactor 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -24
- package/index.ts +7 -0
- package/package.json +2 -1
- package/skills/compactor/SKILL.md +21 -65
- package/skills/compactor-detail/SKILL.md +133 -0
- package/src/commands/index.ts +186 -109
- package/src/compaction/filter-noise.ts +4 -3
- package/src/compaction/hooks.ts +22 -1
- package/src/compaction/search-entries.ts +51 -4
- package/src/config/manager.ts +55 -6
- package/src/config/presets.ts +69 -5
- package/src/config/schema.ts +9 -0
- package/src/index.ts +183 -10
- package/src/info-screen.ts +10 -4
- package/src/security/policy.ts +23 -0
- package/src/session/auto-inject.ts +60 -0
- package/src/session/db.ts +65 -8
- package/src/session/resume-inject.ts +13 -1
- package/src/store/db-base.ts +11 -0
- package/src/store/index.ts +150 -4
- package/src/store/unified.ts +109 -0
- package/src/tools/context-budget.ts +50 -0
- package/src/tools/ctx-batch-execute.ts +2 -5
- package/src/tools/ctx-fetch-and-index.ts +3 -8
- package/src/tools/ctx-index.ts +3 -9
- package/src/tools/ctx-search.ts +3 -7
- package/src/tools/ctx-stats.ts +6 -4
- package/src/tools/register.ts +251 -216
- package/src/tui/settings-overlay.ts +359 -149
- package/src/types.ts +25 -1
- package/skills/compactor-ops/SKILL.md +0 -65
- package/skills/compactor-tools/SKILL.md +0 -120
package/src/commands/index.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Commands perform real work by calling tool implementations directly.
|
|
5
5
|
* Dependencies (sessionDB, contentStore, sessionId) are injected at registration time.
|
|
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.
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
@@ -13,20 +21,25 @@ import { compactTool } from "../tools/compact.js";
|
|
|
13
21
|
import { vccRecall } from "../tools/vcc-recall.js";
|
|
14
22
|
import { ctxStats } from "../tools/ctx-stats.js";
|
|
15
23
|
import { ctxDoctor } from "../tools/ctx-doctor.js";
|
|
16
|
-
import { ctxIndex } from "../tools/ctx-index.js";
|
|
17
24
|
import { ctxSearch } from "../tools/ctx-search.js";
|
|
18
25
|
import { ContentStore } from "../store/index.js";
|
|
19
26
|
import type { SessionDB } from "../session/db.js";
|
|
20
|
-
import type { NormalizedBlock } from "../types.js";
|
|
27
|
+
import type { NormalizedBlock, RuntimeCounters } from "../types.js";
|
|
21
28
|
|
|
22
29
|
export interface CommandDeps {
|
|
23
30
|
sessionDB: SessionDB | null;
|
|
24
31
|
contentStore: ContentStore | null;
|
|
25
32
|
getSessionId: () => string;
|
|
26
33
|
getBlocks: () => NormalizedBlock[];
|
|
34
|
+
getCounters?: () => RuntimeCounters;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function deprecationLog(oldName: string, newName: string): void {
|
|
38
|
+
console.error(`[compactor] DEPRECATED: Command "${oldName}" used — use "${newName}" instead.`);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
42
|
+
// ── /unipi:compact ──────────────────────────────────
|
|
30
43
|
pi.registerCommand("unipi:compact", {
|
|
31
44
|
description: "Trigger manual compaction with stats",
|
|
32
45
|
handler: async (_args: string, ctx: any) => {
|
|
@@ -39,31 +52,42 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
39
52
|
},
|
|
40
53
|
});
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
// ── /unipi:session-recall (new) ─────────────────────
|
|
56
|
+
const sessionRecallHandler = async (args: string, ctx: any) => {
|
|
57
|
+
const query = args.trim();
|
|
58
|
+
if (!query) {
|
|
59
|
+
ctx.ui.notify("Usage: /unipi:session-recall <query>", "warning");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const blocks = deps?.getBlocks() ?? [];
|
|
63
|
+
if (blocks.length === 0) {
|
|
64
|
+
ctx.ui.notify("No session history available for search.", "warning");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = vccRecall(blocks, { query, limit: 10 });
|
|
68
|
+
if (result.hits.length === 0) {
|
|
69
|
+
ctx.ui.notify(`No results for "${query}".`, "info");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const lines = result.hits.map(
|
|
73
|
+
(h, i) => `[${i + 1}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text.slice(0, 200)}`,
|
|
74
|
+
);
|
|
75
|
+
ctx.ui.notify(`Found ${result.total} results:\n${lines.join("\n\n")}`, "info");
|
|
76
|
+
};
|
|
77
|
+
pi.registerCommand("unipi:session-recall", {
|
|
43
78
|
description: "Search session history (BM25 or regex)",
|
|
79
|
+
handler: sessionRecallHandler,
|
|
80
|
+
});
|
|
81
|
+
// Deprecated alias
|
|
82
|
+
pi.registerCommand("unipi:compact-recall", {
|
|
83
|
+
description: "(DEPRECATED) Search session history — use /unipi:session-recall instead",
|
|
44
84
|
handler: async (args: string, ctx: any) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ctx.ui.notify("Usage: /unipi:compact-recall <query>", "warning");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const blocks = deps?.getBlocks() ?? [];
|
|
51
|
-
if (blocks.length === 0) {
|
|
52
|
-
ctx.ui.notify("No session history available for search.", "warning");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const result = vccRecall(blocks, { query, limit: 10 });
|
|
56
|
-
if (result.hits.length === 0) {
|
|
57
|
-
ctx.ui.notify(`No results for "${query}".`, "info");
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const lines = result.hits.map(
|
|
61
|
-
(h, i) => `[${i + 1}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text.slice(0, 200)}`,
|
|
62
|
-
);
|
|
63
|
-
ctx.ui.notify(`Found ${result.total} results:\n${lines.join("\n\n")}`, "info");
|
|
85
|
+
deprecationLog("/unipi:compact-recall", "/unipi:session-recall");
|
|
86
|
+
return sessionRecallHandler(args, ctx);
|
|
64
87
|
},
|
|
65
88
|
});
|
|
66
89
|
|
|
90
|
+
// ── /unipi:compact-stats ─────────────────────────────
|
|
67
91
|
pi.registerCommand("unipi:compact-stats", {
|
|
68
92
|
description: "Show context savings dashboard",
|
|
69
93
|
handler: async (_args: string, ctx: any) => {
|
|
@@ -72,7 +96,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
72
96
|
return;
|
|
73
97
|
}
|
|
74
98
|
try {
|
|
75
|
-
const stats = await ctxStats(deps.sessionDB, deps.contentStore, deps.getSessionId());
|
|
99
|
+
const stats = await ctxStats(deps.sessionDB, deps.contentStore, deps.getSessionId(), deps.getCounters?.());
|
|
76
100
|
const lines = [
|
|
77
101
|
"📊 Compactor Stats",
|
|
78
102
|
`Session events: ${stats.sessionEvents}`,
|
|
@@ -89,6 +113,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
89
113
|
},
|
|
90
114
|
});
|
|
91
115
|
|
|
116
|
+
// ── /unipi:compact-doctor ────────────────────────────
|
|
92
117
|
pi.registerCommand("unipi:compact-doctor", {
|
|
93
118
|
description: "Run diagnostics checklist",
|
|
94
119
|
handler: async (_args: string, ctx: any) => {
|
|
@@ -111,12 +136,14 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
111
136
|
},
|
|
112
137
|
});
|
|
113
138
|
|
|
139
|
+
// ── /unipi:compact-settings ──────────────────────────
|
|
114
140
|
pi.registerCommand("unipi:compact-settings", {
|
|
115
141
|
description: "Open TUI settings overlay",
|
|
116
142
|
handler: async (_args: string, ctx: any) => {
|
|
117
143
|
try {
|
|
144
|
+
const cwd = (ctx as any).cwd ?? process.cwd();
|
|
118
145
|
const { renderSettingsOverlay } = await import("../tui/settings-overlay.js");
|
|
119
|
-
const result = await ctx.ui.custom(renderSettingsOverlay());
|
|
146
|
+
const result = await ctx.ui.custom(renderSettingsOverlay(cwd));
|
|
120
147
|
if (result) {
|
|
121
148
|
ctx.ui.notify("Settings saved.", "info");
|
|
122
149
|
} else {
|
|
@@ -128,12 +155,13 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
128
155
|
},
|
|
129
156
|
});
|
|
130
157
|
|
|
158
|
+
// ── /unipi:compact-preset ────────────────────────────
|
|
131
159
|
pi.registerCommand("unipi:compact-preset", {
|
|
132
|
-
description: "Apply quick preset (
|
|
160
|
+
description: "Apply quick preset (precise/balanced/thorough/lean)",
|
|
133
161
|
handler: async (args: string, ctx: any) => {
|
|
134
162
|
const presetName = parsePreset(args.trim());
|
|
135
163
|
if (!presetName) {
|
|
136
|
-
ctx.ui.notify("Unknown preset. Use:
|
|
164
|
+
ctx.ui.notify("Unknown preset. Use: precise, balanced, thorough, lean", "error");
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
167
|
const config = applyPreset(presetName);
|
|
@@ -146,103 +174,152 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
146
174
|
},
|
|
147
175
|
});
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
files.push(full);
|
|
174
|
-
}
|
|
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);
|
|
175
201
|
}
|
|
176
|
-
} catch {
|
|
177
|
-
// skip unreadable dirs
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
walk(cwd);
|
|
182
|
-
let totalChunks = 0;
|
|
183
|
-
for (const file of files.slice(0, 100)) {
|
|
184
|
-
try {
|
|
185
|
-
const content = readFileSync(file, "utf-8");
|
|
186
|
-
if (content.length < 50) continue;
|
|
187
|
-
const ext = extname(file);
|
|
188
|
-
const ct = ext === ".md" ? "markdown" : ext === ".json" ? "json" : "plain";
|
|
189
|
-
const result = await deps.contentStore.index(relative(cwd, file), content, {
|
|
190
|
-
contentType: ct,
|
|
191
|
-
source: file,
|
|
192
|
-
});
|
|
193
|
-
totalChunks += result.totalChunks;
|
|
194
|
-
} catch {
|
|
195
|
-
// skip unreadable files
|
|
196
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
|
|
197
223
|
}
|
|
198
|
-
ctx.ui.notify(`Indexed ${Math.min(files.length, 100)} files (${totalChunks} chunks).`, "info");
|
|
199
|
-
} catch (err) {
|
|
200
|
-
ctx.ui.notify(`Index error: ${err}`, "error");
|
|
201
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);
|
|
202
239
|
},
|
|
203
240
|
});
|
|
204
241
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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");
|
|
215
257
|
return;
|
|
216
258
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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);
|
|
230
276
|
},
|
|
231
277
|
});
|
|
232
278
|
|
|
233
|
-
|
|
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", {
|
|
234
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
|
+
// ── /unipi:compact-help ──────────────────────────────
|
|
305
|
+
pi.registerCommand("unipi:compact-help", {
|
|
306
|
+
description: "Show detailed compactor documentation (tier-2 skill)",
|
|
235
307
|
handler: async (_args: string, ctx: any) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
// Load tier-2 skill content — delegates to skill loading system
|
|
309
|
+
ctx.ui.notify(
|
|
310
|
+
"🗜️ Compactor Help — Use your compactor-detail skill for full documentation.\n" +
|
|
311
|
+
"Quick commands:\n" +
|
|
312
|
+
" /unipi:compact — trigger compaction\n" +
|
|
313
|
+
" /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
|
+
" /unipi:compact-stats — view stats\n" +
|
|
318
|
+
" /unipi:compact-doctor — run diagnostics\n" +
|
|
319
|
+
" /unipi:compact-settings — TUI settings\n" +
|
|
320
|
+
" /unipi:compact-preset <name> — apply preset",
|
|
321
|
+
"info",
|
|
322
|
+
);
|
|
246
323
|
},
|
|
247
324
|
});
|
|
248
325
|
}
|
|
@@ -17,9 +17,10 @@ const NOISE_STRINGS = [
|
|
|
17
17
|
|
|
18
18
|
const XML_WRAPPER_RE = /<(system-reminder|ide_opened_file|command-message|context-window-usage)[^>]*>[\s\S]*?<\/\1>/g;
|
|
19
19
|
|
|
20
|
-
const isNoiseUserBlock = (text: string): boolean => {
|
|
20
|
+
const isNoiseUserBlock = (text: string, extraPatterns: string[] = []): boolean => {
|
|
21
21
|
const trimmed = text.trim();
|
|
22
22
|
if (NOISE_STRINGS.some((s) => trimmed.includes(s))) return true;
|
|
23
|
+
if (extraPatterns.length > 0 && extraPatterns.some((p) => trimmed.includes(p))) return true;
|
|
23
24
|
const stripped = trimmed.replace(XML_WRAPPER_RE, "").trim();
|
|
24
25
|
return stripped.length === 0;
|
|
25
26
|
};
|
|
@@ -27,14 +28,14 @@ const isNoiseUserBlock = (text: string): boolean => {
|
|
|
27
28
|
const cleanUserText = (text: string): string =>
|
|
28
29
|
text.replace(XML_WRAPPER_RE, "").trim();
|
|
29
30
|
|
|
30
|
-
export const filterNoise = (blocks: NormalizedBlock[]): NormalizedBlock[] => {
|
|
31
|
+
export const filterNoise = (blocks: NormalizedBlock[], extraPatterns?: string[]): NormalizedBlock[] => {
|
|
31
32
|
const out: NormalizedBlock[] = [];
|
|
32
33
|
for (const b of blocks) {
|
|
33
34
|
if (b.kind === "thinking") continue;
|
|
34
35
|
if (b.kind === "tool_call" && NOISE_TOOLS.has(b.name)) continue;
|
|
35
36
|
if (b.kind === "tool_result" && NOISE_TOOLS.has(b.name)) continue;
|
|
36
37
|
if (b.kind === "user") {
|
|
37
|
-
if (isNoiseUserBlock(b.text)) continue;
|
|
38
|
+
if (isNoiseUserBlock(b.text, extraPatterns)) continue;
|
|
38
39
|
const cleaned = cleanUserText(b.text);
|
|
39
40
|
if (!cleaned) continue;
|
|
40
41
|
out.push({ kind: "user", text: cleaned });
|
package/src/compaction/hooks.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { compile } from "./summarize.js";
|
|
|
8
8
|
import { loadConfig } from "../config/manager.js";
|
|
9
9
|
import { buildOwnCut } from "./cut.js";
|
|
10
10
|
import type { CompactionStats } from "../types.js";
|
|
11
|
+
import type { SessionDB } from "../session/db.js";
|
|
11
12
|
|
|
12
13
|
export const COMPACTOR_INSTRUCTION = "__compactor__";
|
|
13
14
|
|
|
@@ -50,7 +51,10 @@ const REASON_MESSAGES: Record<import("./cut.js").OwnCutCancelReason, string> = {
|
|
|
50
51
|
no_user_message: "compactor: Cannot compact — no user message found",
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
export function registerCompactionHooks(
|
|
54
|
+
export function registerCompactionHooks(
|
|
55
|
+
pi: ExtensionAPI,
|
|
56
|
+
deps?: { getSessionDB?: () => SessionDB | null; getSessionId?: () => string },
|
|
57
|
+
): void {
|
|
54
58
|
pi.on("session_before_compact", (event, ctx) => {
|
|
55
59
|
const { preparation, branchEntries, customInstructions } = event;
|
|
56
60
|
const config = loadConfig();
|
|
@@ -96,6 +100,23 @@ export function registerCompactionHooks(pi: ExtensionAPI): void {
|
|
|
96
100
|
keptTokensEst: Math.round(keptChars / 4),
|
|
97
101
|
};
|
|
98
102
|
|
|
103
|
+
// Persist cumulative compaction stats
|
|
104
|
+
const sessionDB = deps?.getSessionDB?.();
|
|
105
|
+
if (sessionDB && deps?.getSessionId) {
|
|
106
|
+
try {
|
|
107
|
+
const sessionId = deps.getSessionId();
|
|
108
|
+
const charsBefore = agentMessages.reduce((sum: number, msg: any) => {
|
|
109
|
+
const c = msg.message?.content;
|
|
110
|
+
if (typeof c === "string") return sum + c.length;
|
|
111
|
+
if (Array.isArray(c)) return sum + c.reduce((s: number, p: any) => s + (p.text?.length ?? 0), 0);
|
|
112
|
+
return sum;
|
|
113
|
+
}, 0);
|
|
114
|
+
sessionDB.addCompactionStats(sessionId, charsBefore, keptChars, agentMessages.length);
|
|
115
|
+
} catch {
|
|
116
|
+
// non-fatal
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
dbg(config.debug, "compile", { messageCount: messages.length, hasPrevSummary: !!preparation.previousSummary });
|
|
100
121
|
const summary = compile({
|
|
101
122
|
messages,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BM25-lite search over normalized message blocks
|
|
3
|
+
*
|
|
4
|
+
* Includes module-level index cache for fast repeated queries.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { NormalizedBlock } from "../types.js";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
6
9
|
|
|
7
10
|
interface SearchDoc {
|
|
8
11
|
id: number;
|
|
@@ -63,6 +66,23 @@ export interface SearchHit {
|
|
|
63
66
|
kind: string;
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
// Module-level index cache
|
|
70
|
+
let cachedIndexHash = "";
|
|
71
|
+
let cachedDocs: SearchDoc[] = [];
|
|
72
|
+
let cachedIndex: Map<string, number[]> | null = null;
|
|
73
|
+
let cachedDocCount = 0;
|
|
74
|
+
let cachedAvgDocLen = 0;
|
|
75
|
+
let cachedDocLens: Map<number, number> = new Map();
|
|
76
|
+
|
|
77
|
+
export function invalidateSearchCache(): void {
|
|
78
|
+
cachedIndexHash = "";
|
|
79
|
+
cachedDocs = [];
|
|
80
|
+
cachedIndex = null;
|
|
81
|
+
cachedDocCount = 0;
|
|
82
|
+
cachedAvgDocLen = 0;
|
|
83
|
+
cachedDocLens = new Map();
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
export function searchEntries(
|
|
67
87
|
blocks: NormalizedBlock[],
|
|
68
88
|
query: string,
|
|
@@ -74,10 +94,37 @@ export function searchEntries(
|
|
|
74
94
|
kind: b.kind,
|
|
75
95
|
}));
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
// Compute content hash to detect blocks change
|
|
98
|
+
const hashSource = docs.length > 0
|
|
99
|
+
? `${docs.length}:${docs[0].text.slice(0, 80)}:${docs[docs.length - 1].text.slice(-80)}`
|
|
100
|
+
: "empty";
|
|
101
|
+
const currentHash = createHash("sha256").update(hashSource).digest("hex");
|
|
102
|
+
|
|
103
|
+
// Use cached index if blocks haven't changed
|
|
104
|
+
let index: Map<string, number[]>;
|
|
105
|
+
let docCount: number;
|
|
106
|
+
let avgDocLen: number;
|
|
107
|
+
let docLens: Map<number, number>;
|
|
108
|
+
|
|
109
|
+
if (currentHash === cachedIndexHash && cachedIndex) {
|
|
110
|
+
index = cachedIndex;
|
|
111
|
+
docCount = cachedDocCount;
|
|
112
|
+
avgDocLen = cachedAvgDocLen;
|
|
113
|
+
docLens = cachedDocLens;
|
|
114
|
+
} else {
|
|
115
|
+
index = buildIndex(docs);
|
|
116
|
+
docCount = docs.length;
|
|
117
|
+
docLens = new Map(docs.map((d) => [d.id, tokenize(d.text).length]));
|
|
118
|
+
avgDocLen = docCount > 0 ? [...docLens.values()].reduce((a, b) => a + b, 0) / docCount : 1;
|
|
119
|
+
|
|
120
|
+
// Update cache
|
|
121
|
+
cachedIndexHash = currentHash;
|
|
122
|
+
cachedDocs = docs;
|
|
123
|
+
cachedIndex = index;
|
|
124
|
+
cachedDocCount = docCount;
|
|
125
|
+
cachedAvgDocLen = avgDocLen;
|
|
126
|
+
cachedDocLens = docLens;
|
|
127
|
+
}
|
|
81
128
|
|
|
82
129
|
const queryTokens = tokenize(query);
|
|
83
130
|
if (queryTokens.length === 0) return [];
|
package/src/config/manager.ts
CHANGED
|
@@ -10,6 +10,11 @@ import { DEFAULT_COMPACTOR_CONFIG } from "./schema.js";
|
|
|
10
10
|
|
|
11
11
|
export const COMPACTOR_CONFIG_PATH = join(homedir(), ".unipi", "config", "compactor", "config.json");
|
|
12
12
|
|
|
13
|
+
/** Return the per-project config path for a given project directory. */
|
|
14
|
+
export function projectConfigPath(cwd: string): string {
|
|
15
|
+
return join(cwd, ".unipi", "config", "compactor.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
const readJson = (path: string): Record<string, unknown> | null => {
|
|
14
19
|
try {
|
|
15
20
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -18,23 +23,66 @@ const readJson = (path: string): Record<string, unknown> | null => {
|
|
|
18
23
|
}
|
|
19
24
|
};
|
|
20
25
|
|
|
26
|
+
/** Deep merge project overrides into global config. */
|
|
27
|
+
function deepMerge<T extends Record<string, any>>(base: T, override: Partial<T>): T {
|
|
28
|
+
const result = { ...base };
|
|
29
|
+
for (const key of Object.keys(override) as (keyof T)[]) {
|
|
30
|
+
const baseVal = result[key];
|
|
31
|
+
const overrideVal = override[key];
|
|
32
|
+
if (
|
|
33
|
+
overrideVal !== undefined &&
|
|
34
|
+
typeof overrideVal === "object" &&
|
|
35
|
+
!Array.isArray(overrideVal) &&
|
|
36
|
+
overrideVal !== null &&
|
|
37
|
+
typeof baseVal === "object" &&
|
|
38
|
+
!Array.isArray(baseVal) &&
|
|
39
|
+
baseVal !== null
|
|
40
|
+
) {
|
|
41
|
+
(result as any)[key] = deepMerge(baseVal as any, overrideVal as any);
|
|
42
|
+
} else if (overrideVal !== undefined) {
|
|
43
|
+
(result as any)[key] = overrideVal;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
/**
|
|
22
50
|
* Load compactor config from disk with defaults fallback.
|
|
51
|
+
* Supports per-project overrides at <cwd>/.unipi/config/compactor.json.
|
|
23
52
|
*/
|
|
24
|
-
export function loadConfig(): CompactorConfig {
|
|
53
|
+
export function loadConfig(cwd?: string): CompactorConfig {
|
|
25
54
|
const parsed = readJson(COMPACTOR_CONFIG_PATH);
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
let config: CompactorConfig;
|
|
56
|
+
if (!parsed || typeof parsed !== "object") {
|
|
57
|
+
config = structuredClone(DEFAULT_COMPACTOR_CONFIG);
|
|
58
|
+
} else {
|
|
59
|
+
config = migrateConfig(parsed as Partial<CompactorConfig>);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Apply per-project overrides if cwd is provided and project config exists
|
|
63
|
+
if (cwd) {
|
|
64
|
+
const projPath = projectConfigPath(cwd);
|
|
65
|
+
const projOverride = readJson(projPath);
|
|
66
|
+
if (projOverride && typeof projOverride === "object") {
|
|
67
|
+
config = deepMerge(config, projOverride as Partial<CompactorConfig>);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return config;
|
|
28
72
|
}
|
|
29
73
|
|
|
30
74
|
/**
|
|
31
75
|
* Save config to disk with schema validation.
|
|
76
|
+
* If perProject is true, saves to <cwd>/.unipi/config/compactor.json instead of global.
|
|
32
77
|
*/
|
|
33
|
-
export function saveConfig(config: CompactorConfig): { success: boolean; error?: string } {
|
|
78
|
+
export function saveConfig(config: CompactorConfig, opts?: { perProject?: boolean; cwd?: string }): { success: boolean; error?: string } {
|
|
34
79
|
try {
|
|
35
|
-
const
|
|
80
|
+
const targetPath = (opts?.perProject && opts?.cwd)
|
|
81
|
+
? projectConfigPath(opts.cwd)
|
|
82
|
+
: COMPACTOR_CONFIG_PATH;
|
|
83
|
+
const dir = dirname(targetPath);
|
|
36
84
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
-
writeFileSync(
|
|
85
|
+
writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
38
86
|
return { success: true };
|
|
39
87
|
} catch (err) {
|
|
40
88
|
return { success: false, error: String(err) };
|
|
@@ -67,6 +115,7 @@ export function migrateConfig(partial: Partial<CompactorConfig>): CompactorConfi
|
|
|
67
115
|
fts5Index: mergeStrategy("fts5Index", defaults.fts5Index, partial.fts5Index),
|
|
68
116
|
sandboxExecution: mergeStrategy("sandboxExecution", defaults.sandboxExecution, partial.sandboxExecution),
|
|
69
117
|
toolDisplay: mergeStrategy("toolDisplay", defaults.toolDisplay, partial.toolDisplay),
|
|
118
|
+
pipeline: mergeStrategy("pipeline", defaults.pipeline, (partial as any).pipeline) as any,
|
|
70
119
|
overrideDefaultCompaction: partial.overrideDefaultCompaction ?? defaults.overrideDefaultCompaction,
|
|
71
120
|
debug: partial.debug ?? defaults.debug,
|
|
72
121
|
showTruncationHints: partial.showTruncationHints ?? defaults.showTruncationHints,
|