@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 CHANGED
@@ -1,14 +1,8 @@
1
1
  # @pi-unipi/compactor
2
2
 
3
- Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuity, sandbox execution, FTS5 search, and tool display optimization into a single cohesive package.
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
- ## Features
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 FTS5 index |
37
+ | `content_index` | content | Chunk content to FTS5 index |
38
38
  | `content_search` | content | Query indexed content |
39
- | `content_fetch` | content | Fetch URL index |
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 + critical rules + Ralph awareness.
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
- ## Configuration
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: `←→` cycle modes, `Space` toggle, `s` save, `Esc` cancel
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, FTS5 search, and tool display optimization",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
- registerCompactionHooks(pi);
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
- const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
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
- const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
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
- // charsBefore = total bytes processed by all tools (proxy for context window usage)
308
- // charsKept = bytes that stayed in context (bytesReturned, minus what compaction removed)
309
- const tokensBefore = (event as any).tokensBefore ?? 0;
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 if (tokensBefore > 0) {
320
- // Fallback: only tokensBefore available, use conservative estimate
321
- const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
322
- counters.totalTokensCompacted += tokensBefore - tokensAfter;
323
- sessionDB.addCompactionStats(sessionId, tokensBefore * 4, tokensAfter * 4, 1);
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
- const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
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
 
@@ -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.compact_count > 0
132
- ? `${report.continuity.compact_count} compaction(s) this session`
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: {
@@ -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
 
@@ -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: counters?.compactions ?? sessionStats?.compact_count ?? 0,
32
- tokensSaved: counters?.totalTokensCompacted ?? 0,
55
+ compactions,
56
+ tokensSaved,
33
57
  compressionRatio: "N/A",
34
58
  indexedDocs: storeStats.sources,
35
59
  indexedChunks: storeStats.chunks,