@pi-unipi/compactor 0.2.2 → 0.2.3
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 +13 -25
- package/package.json +1 -1
- package/src/index.ts +34 -15
- package/src/info-screen.ts +3 -3
- package/src/session/analytics.ts +8 -1
- package/src/tools/ctx-stats.ts +26 -2
package/README.md
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
# @pi-unipi/compactor
|
|
2
2
|
|
|
3
|
-
Context engine
|
|
3
|
+
Context engine that keeps sessions lean. Compacts conversations, indexes code, searches history, and runs sandboxed code — all without burning LLM tokens on compaction.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Zero-LLM Compaction** — 6-stage pipeline (normalize → filter → build sections → brief → format → merge) achieves 95%+ token reduction with zero API cost
|
|
8
|
-
- **Session Continuity** — XML resume snapshots survive compaction, preserving context across session boundaries
|
|
9
|
-
- **Sandbox Execution** — 11 languages with process isolation, security hardening, and output capping
|
|
10
|
-
- **FTS5 Search** — Full-text search over indexed content with auto-chunking
|
|
11
|
-
- **Tool Display** — Mode-aware rendering for read, grep, find, ls, bash, edit, write tools
|
|
5
|
+
The zero-LLM pipeline compresses context through 6 stages (normalize, filter, build sections, brief, format, merge) to hit 95%+ token reduction at zero API cost. Session continuity preserves context across compaction boundaries with XML resume snapshots.
|
|
12
6
|
|
|
13
7
|
## Commands
|
|
14
8
|
|
|
@@ -25,6 +19,12 @@ Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuit
|
|
|
25
19
|
| `/unipi:compact-preset <name>` | Apply quick preset |
|
|
26
20
|
| `/unipi:compact-help` | Show detailed documentation |
|
|
27
21
|
|
|
22
|
+
## Special Triggers
|
|
23
|
+
|
|
24
|
+
Compactor tools are available to the main agent when installed. All workflow skills can use compactor tools for context management.
|
|
25
|
+
|
|
26
|
+
Compactor registers with the info-screen dashboard, showing compaction count, tokens saved, compression ratio, and indexed documents. The footer subscribes to `COMPACTOR_STATSUPDATED` events to display compaction stats in the status bar.
|
|
27
|
+
|
|
28
28
|
## Agent Tools
|
|
29
29
|
|
|
30
30
|
| Tool | Family | Description |
|
|
@@ -34,19 +34,19 @@ Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuit
|
|
|
34
34
|
| `sandbox` | sandbox | Run code in sandbox (11 languages) |
|
|
35
35
|
| `sandbox_file` | sandbox | Execute file via FILE_CONTENT |
|
|
36
36
|
| `sandbox_batch` | sandbox | Atomic batch of commands + searches |
|
|
37
|
-
| `content_index` | content | Chunk content
|
|
37
|
+
| `content_index` | content | Chunk content to FTS5 index |
|
|
38
38
|
| `content_search` | content | Query indexed content |
|
|
39
|
-
| `content_fetch` | content | Fetch URL
|
|
39
|
+
| `content_fetch` | content | Fetch URL and index |
|
|
40
40
|
| `compactor_stats` | compactor | Context savings dashboard |
|
|
41
41
|
| `compactor_doctor` | compactor | Diagnostics checklist |
|
|
42
42
|
| `context_budget` | compactor | Estimate remaining context window |
|
|
43
43
|
|
|
44
44
|
## Two-Tier Skills
|
|
45
45
|
|
|
46
|
-
- **Tier 1** (`compactor`): ~175 tokens, always loaded. Routing
|
|
46
|
+
- **Tier 1** (`compactor`): ~175 tokens, always loaded. Routing and critical rules.
|
|
47
47
|
- **Tier 2** (`compactor-detail`): On-demand. Full tool reference, anti-patterns, sandbox languages, FTS5 modes, workflows.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Configurables
|
|
50
50
|
|
|
51
51
|
Config lives at `~/.unipi/config/compactor/config.json`. Per-project overrides at `<project>/.unipi/config/compactor.json`.
|
|
52
52
|
|
|
@@ -78,7 +78,7 @@ Tabbed settings interface (Presets / Strategies / Pipeline):
|
|
|
78
78
|
- `/` key opens search filter in Strategies tab
|
|
79
79
|
- Preset selection shows 3-line preview
|
|
80
80
|
- Per-project override checkbox (`o` key)
|
|
81
|
-
- Keyboard:
|
|
81
|
+
- Keyboard: left/right cycle modes, Space toggle, `s` save, Esc cancel
|
|
82
82
|
|
|
83
83
|
## Architecture
|
|
84
84
|
|
|
@@ -95,18 +95,6 @@ Tabbed settings interface (Presets / Strategies / Pipeline):
|
|
|
95
95
|
└─────────────────────┘
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
## Installation
|
|
99
|
-
|
|
100
|
-
Included in `@pi-unipi/unipi` metapackage. To use standalone:
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"pi": {
|
|
105
|
-
"extensions": ["node_modules/@pi-unipi/compactor/src/index.ts"]
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
98
|
## License
|
|
111
99
|
|
|
112
100
|
MIT
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -117,7 +117,12 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
117
117
|
executor = new PolyglotExecutor();
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
// Register compaction hooks with lazy deps — sessionDB/sessionId may not be
|
|
121
|
+
// available at registration time, but will be by the time events fire.
|
|
122
|
+
registerCompactionHooks(pi, {
|
|
123
|
+
getSessionDB: () => sessionDB,
|
|
124
|
+
getSessionId: () => currentSessionId,
|
|
125
|
+
});
|
|
121
126
|
|
|
122
127
|
// Commands registered inside session_start after init() when deps are ready
|
|
123
128
|
const getCommandDeps = () => ({
|
|
@@ -283,7 +288,9 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
283
288
|
|
|
284
289
|
pi.on("session_before_compact", async (event, _ctx) => {
|
|
285
290
|
if (sessionDB) {
|
|
286
|
-
|
|
291
|
+
// Use closure currentSessionId — Pi's session_before_compact event
|
|
292
|
+
// does not include sessionId at the top level.
|
|
293
|
+
const sessionId = currentSessionId;
|
|
287
294
|
const events = sessionDB.getEvents(sessionId, { limit: 1000 });
|
|
288
295
|
const stats = sessionDB.getSessionStats(sessionId);
|
|
289
296
|
debug("session_before_compact", { sessionId, eventCount: events.length, compactCount: stats?.compact_count ?? 0 });
|
|
@@ -297,18 +304,23 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
297
304
|
|
|
298
305
|
pi.on("session_compact", async (event, _ctx) => {
|
|
299
306
|
if (sessionDB) {
|
|
300
|
-
|
|
307
|
+
// Use closure currentSessionId — Pi's session_compact event does not
|
|
308
|
+
// include sessionId at the top level (it's inside compactionEntry).
|
|
309
|
+
const sessionId = currentSessionId;
|
|
301
310
|
sessionDB.incrementCompactCount(sessionId);
|
|
302
311
|
counters.compactions++;
|
|
303
312
|
|
|
313
|
+
// Pi's session_compact event structure: { compactionEntry, fromExtension }
|
|
314
|
+
// tokensBefore is inside compactionEntry, not at event root.
|
|
315
|
+
const compactionEntry = (event as any).compactionEntry;
|
|
316
|
+
const tokensBefore = compactionEntry?.tokensBefore ?? 0;
|
|
317
|
+
|
|
304
318
|
// Use actual runtimeStats for byte measurement instead of heuristic
|
|
305
319
|
const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce((s, b) => s + b, 0);
|
|
306
320
|
const totalBytesProcessed = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed + totalBytesReturned;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (totalBytesProcessed > 0 && tokensBefore > 0) {
|
|
311
|
-
// Use actual token count from Pi, estimate chars from it
|
|
321
|
+
|
|
322
|
+
if (tokensBefore > 0) {
|
|
323
|
+
// Use actual token count from Pi's compactionEntry
|
|
312
324
|
const charsBefore = tokensBefore * 4;
|
|
313
325
|
// Estimate kept chars: proportional to what remains after compaction
|
|
314
326
|
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
|
|
@@ -316,13 +328,19 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
316
328
|
const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
|
|
317
329
|
counters.totalTokensCompacted += tokensBefore - tokensAfter;
|
|
318
330
|
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
|
|
319
|
-
} else
|
|
320
|
-
// Fallback:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
} else {
|
|
332
|
+
// Fallback: estimate from runtime byte stats when tokensBefore unavailable
|
|
333
|
+
if (totalBytesProcessed > 0) {
|
|
334
|
+
const charsBefore = totalBytesProcessed;
|
|
335
|
+
const charsKept = totalBytesReturned;
|
|
336
|
+
const messagesSummarized = Math.max(1, Math.round(totalBytesProcessed / 2000));
|
|
337
|
+
const estTokensBefore = Math.round(totalBytesProcessed / 4);
|
|
338
|
+
const estTokensAfter = Math.round(totalBytesReturned / 4);
|
|
339
|
+
counters.totalTokensCompacted += Math.max(0, estTokensBefore - estTokensAfter);
|
|
340
|
+
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
|
|
341
|
+
}
|
|
324
342
|
}
|
|
325
|
-
debug("session_compact", { sessionId, tokensBefore, totalBytesProcessed });
|
|
343
|
+
debug("session_compact", { sessionId, tokensBefore, totalBytesProcessed, hasCompactionEntry: !!compactionEntry });
|
|
326
344
|
}
|
|
327
345
|
});
|
|
328
346
|
|
|
@@ -412,7 +430,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
412
430
|
|
|
413
431
|
pi.on("tool_result", async (event, _ctx) => {
|
|
414
432
|
if (!sessionDB) return;
|
|
415
|
-
|
|
433
|
+
// Use closure currentSessionId — tool_result events use the same session
|
|
434
|
+
const sessionId = currentSessionId;
|
|
416
435
|
const toolNameRaw = (event as any).toolName ?? "";
|
|
417
436
|
const isError = (event as any).isError ?? false;
|
|
418
437
|
|
package/src/info-screen.ts
CHANGED
|
@@ -125,11 +125,11 @@ export async function getInfoScreenData(
|
|
|
125
125
|
detail: top5Detail,
|
|
126
126
|
},
|
|
127
127
|
compactions: {
|
|
128
|
-
value: String(report.continuity.compact_count),
|
|
128
|
+
value: String(Math.max(report.continuity.compact_count, report.continuity.all_time_compact_count)),
|
|
129
129
|
detail: compactStats
|
|
130
130
|
? `Last: ${compactStats.summarized} msgs summarized, ${compactStats.kept} kept (~${formatTokens(compactStats.keptTokensEst)} tok)`
|
|
131
|
-
: report.continuity.
|
|
132
|
-
? `${report.continuity.
|
|
131
|
+
: report.continuity.all_time_compact_count > 0
|
|
132
|
+
? `${report.continuity.all_time_compact_count} compaction(s) across all sessions`
|
|
133
133
|
: "No compactions yet",
|
|
134
134
|
},
|
|
135
135
|
toolCalls: {
|
package/src/session/analytics.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface FullReport {
|
|
|
66
66
|
continuity: {
|
|
67
67
|
total_events: number;
|
|
68
68
|
compact_count: number;
|
|
69
|
+
all_time_compact_count: number;
|
|
69
70
|
resume_ready: boolean;
|
|
70
71
|
};
|
|
71
72
|
/** Persistent project memory — all events across all sessions */
|
|
@@ -135,6 +136,11 @@ export class AnalyticsEngine {
|
|
|
135
136
|
).get(sid) as { compact_count: number } | undefined;
|
|
136
137
|
const compactCount = meta?.compact_count ?? 0;
|
|
137
138
|
|
|
139
|
+
// ── All-time compaction count across all sessions ──
|
|
140
|
+
const allTimeCompactions = (this.db.prepare(
|
|
141
|
+
"SELECT COALESCE(SUM(compact_count), 0) as total FROM session_meta",
|
|
142
|
+
).get() as { total: number }).total;
|
|
143
|
+
|
|
138
144
|
const resume = this.db.prepare(
|
|
139
145
|
"SELECT event_count, consumed FROM session_resume WHERE session_id = ? ORDER BY created_at DESC LIMIT 1",
|
|
140
146
|
).get(sid) as { event_count: number; consumed: number } | undefined;
|
|
@@ -165,6 +171,7 @@ export class AnalyticsEngine {
|
|
|
165
171
|
continuity: {
|
|
166
172
|
total_events: eventTotal,
|
|
167
173
|
compact_count: compactCount,
|
|
174
|
+
all_time_compact_count: allTimeCompactions,
|
|
168
175
|
resume_ready: resumeReady,
|
|
169
176
|
},
|
|
170
177
|
projectMemory: {
|
|
@@ -188,7 +195,7 @@ export function createMinimalDb(): DatabaseAdapter {
|
|
|
188
195
|
// so AnalyticsEngine queries don't fail.
|
|
189
196
|
const emptyStmt = {
|
|
190
197
|
run: (..._params: unknown[]) => {},
|
|
191
|
-
get: (..._params: unknown[]) => ({ cnt: 0, sessions: 0, compact_count: 0, session_id: "", event_count: 0, consumed: 1 }),
|
|
198
|
+
get: (..._params: unknown[]) => ({ cnt: 0, sessions: 0, compact_count: 0, total: 0, session_id: "", event_count: 0, consumed: 1 }),
|
|
192
199
|
all: (..._params: unknown[]) => [] as unknown[],
|
|
193
200
|
};
|
|
194
201
|
|
package/src/tools/ctx-stats.ts
CHANGED
|
@@ -26,10 +26,34 @@ export async function ctxStats(
|
|
|
26
26
|
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
27
27
|
const storeStats = await contentStore.getStats();
|
|
28
28
|
|
|
29
|
+
// Compute tokensSaved: prefer in-memory counters (current session),
|
|
30
|
+
// fall back to per-session DB stats, then all-time DB stats.
|
|
31
|
+
let tokensSaved = counters?.totalTokensCompacted ?? 0;
|
|
32
|
+
if (tokensSaved === 0 && sessionStats) {
|
|
33
|
+
const sessionCharsBefore = (sessionStats as any).total_chars_before ?? 0;
|
|
34
|
+
const sessionCharsKept = (sessionStats as any).total_chars_kept ?? 0;
|
|
35
|
+
tokensSaved = Math.round((sessionCharsBefore - sessionCharsKept) / 4);
|
|
36
|
+
}
|
|
37
|
+
if (tokensSaved === 0) {
|
|
38
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
39
|
+
tokensSaved = Math.round((allTime.allCharsBefore - allTime.allCharsKept) / 4);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Compute compactions: prefer in-memory counter (current session),
|
|
43
|
+
// fall back to per-session DB, then all-time DB.
|
|
44
|
+
let compactions = counters?.compactions ?? 0;
|
|
45
|
+
if (compactions === 0) {
|
|
46
|
+
compactions = sessionStats?.compact_count ?? 0;
|
|
47
|
+
}
|
|
48
|
+
if (compactions === 0) {
|
|
49
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
50
|
+
compactions = allTime.allCompactions;
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
return {
|
|
30
54
|
sessionEvents: sessionStats?.event_count ?? 0,
|
|
31
|
-
compactions
|
|
32
|
-
tokensSaved
|
|
55
|
+
compactions,
|
|
56
|
+
tokensSaved,
|
|
33
57
|
compressionRatio: "N/A",
|
|
34
58
|
indexedDocs: storeStats.sources,
|
|
35
59
|
indexedChunks: storeStats.chunks,
|