@pi-unipi/compactor 0.2.2 → 2.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/README.md +16 -26
- 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 +56 -48
- package/src/info-screen.ts +97 -40
- package/src/session/analytics.ts +8 -1
- 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 +69 -12
- 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/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
|
package/src/config/presets.ts
CHANGED
|
@@ -11,16 +11,16 @@ const preset = (
|
|
|
11
11
|
): CompactorConfig => ({
|
|
12
12
|
...structuredClone(DEFAULT_COMPACTOR_CONFIG),
|
|
13
13
|
...overrides,
|
|
14
|
-
sessionGoals: { ...DEFAULT_COMPACTOR_CONFIG.sessionGoals, ...(overrides.sessionGoals
|
|
15
|
-
filesAndChanges: { ...DEFAULT_COMPACTOR_CONFIG.filesAndChanges, ...(overrides.filesAndChanges
|
|
16
|
-
commits: { ...DEFAULT_COMPACTOR_CONFIG.commits, ...(overrides.commits
|
|
17
|
-
outstandingContext: { ...DEFAULT_COMPACTOR_CONFIG.outstandingContext, ...(overrides.outstandingContext
|
|
18
|
-
userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, ...(overrides.userPreferences
|
|
19
|
-
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, ...(overrides.briefTranscript
|
|
20
|
-
sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, ...(overrides.sessionContinuity
|
|
21
|
-
fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, ...(overrides.fts5Index
|
|
22
|
-
sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, ...(overrides.sandboxExecution
|
|
23
|
-
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, ...(overrides.toolDisplay
|
|
14
|
+
sessionGoals: { ...DEFAULT_COMPACTOR_CONFIG.sessionGoals, ...(overrides.sessionGoals ?? {}) },
|
|
15
|
+
filesAndChanges: { ...DEFAULT_COMPACTOR_CONFIG.filesAndChanges, ...(overrides.filesAndChanges ?? {}) },
|
|
16
|
+
commits: { ...DEFAULT_COMPACTOR_CONFIG.commits, ...(overrides.commits ?? {}) },
|
|
17
|
+
outstandingContext: { ...DEFAULT_COMPACTOR_CONFIG.outstandingContext, ...(overrides.outstandingContext ?? {}) },
|
|
18
|
+
userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, ...(overrides.userPreferences ?? {}) },
|
|
19
|
+
briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, ...(overrides.briefTranscript ?? {}) },
|
|
20
|
+
sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, ...(overrides.sessionContinuity ?? {}) },
|
|
21
|
+
fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, ...(overrides.fts5Index ?? {}) },
|
|
22
|
+
sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, ...(overrides.sandboxExecution ?? {}) },
|
|
23
|
+
toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, ...(overrides.toolDisplay ?? {}) },
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
// Pipeline feature defaults per preset:
|
package/src/executor/executor.ts
CHANGED
|
@@ -122,11 +122,11 @@ export class PolyglotExecutor {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
return result;
|
|
125
|
-
} catch (err:
|
|
125
|
+
} catch (err: unknown) {
|
|
126
126
|
try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
127
127
|
return {
|
|
128
128
|
stdout: "",
|
|
129
|
-
stderr: err
|
|
129
|
+
stderr: (err instanceof Error ? err.message : String(err)),
|
|
130
130
|
exitCode: 1,
|
|
131
131
|
timedOut: false,
|
|
132
132
|
};
|
|
@@ -228,10 +228,10 @@ export class PolyglotExecutor {
|
|
|
228
228
|
timeout: timeout / 2,
|
|
229
229
|
stdio: ["ignore", "pipe", "pipe"],
|
|
230
230
|
});
|
|
231
|
-
} catch (err:
|
|
231
|
+
} catch (err: unknown) {
|
|
232
232
|
return {
|
|
233
233
|
stdout: "",
|
|
234
|
-
stderr: err
|
|
234
|
+
stderr: (err instanceof Error && 'stderr' in err ? String((err as Error & { stderr?: Buffer }).stderr) : '') || (err instanceof Error ? err.message : String(err)) || "Rust compilation failed",
|
|
235
235
|
exitCode: 1,
|
|
236
236
|
timedOut: false,
|
|
237
237
|
};
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { registerCompactionHooks } from "./compaction/hooks.js";
|
|
|
9
9
|
import { SessionDB, getWorktreeSuffix } from "./session/db.js";
|
|
10
10
|
import { extractEventsFromToolResult } from "./session/extract.js";
|
|
11
11
|
import { injectResumeSnapshot } from "./session/resume-inject.js";
|
|
12
|
-
import { ContentStore } from "./store/index.js";
|
|
13
12
|
import { PolyglotExecutor } from "./executor/executor.js";
|
|
14
13
|
import { registerCommands } from "./commands/index.js";
|
|
15
14
|
import { registerCompactorTools } from "./tools/register.js";
|
|
@@ -57,7 +56,6 @@ function isIndexTool(_name: string): boolean {
|
|
|
57
56
|
|
|
58
57
|
export default function compactorExtension(pi: ExtensionAPI): void {
|
|
59
58
|
let sessionDB: SessionDB | null = null;
|
|
60
|
-
let contentStore: ContentStore | null = null;
|
|
61
59
|
let executor: PolyglotExecutor | null = null;
|
|
62
60
|
let config = loadConfig();
|
|
63
61
|
let cachedBlocks: NormalizedBlock[] = [];
|
|
@@ -101,28 +99,19 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
101
99
|
sessionDB = null;
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
// Initialize ContentStore independently — its failure shouldn't
|
|
105
|
-
// prevent SessionDB commands from working.
|
|
106
|
-
if (config.fts5Index.enabled) {
|
|
107
|
-
try {
|
|
108
|
-
const cs = new ContentStore();
|
|
109
|
-
await cs.init();
|
|
110
|
-
contentStore = cs;
|
|
111
|
-
} catch {
|
|
112
|
-
// Silently ignore — ContentStore init failure is handled gracefully.
|
|
113
|
-
contentStore = null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
102
|
executor = new PolyglotExecutor();
|
|
118
103
|
};
|
|
119
104
|
|
|
120
|
-
|
|
105
|
+
// Register compaction hooks with lazy deps — sessionDB/sessionId may not be
|
|
106
|
+
// available at registration time, but will be by the time events fire.
|
|
107
|
+
registerCompactionHooks(pi, {
|
|
108
|
+
getSessionDB: () => sessionDB,
|
|
109
|
+
getSessionId: () => currentSessionId,
|
|
110
|
+
});
|
|
121
111
|
|
|
122
112
|
// Commands registered inside session_start after init() when deps are ready
|
|
123
113
|
const getCommandDeps = () => ({
|
|
124
114
|
sessionDB,
|
|
125
|
-
contentStore,
|
|
126
115
|
getSessionId: () => currentSessionId,
|
|
127
116
|
getBlocks: () => cachedBlocks,
|
|
128
117
|
getCounters,
|
|
@@ -139,6 +128,17 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
139
128
|
|
|
140
129
|
debug("session_start", { sessionId: fullSessionId, projectDir });
|
|
141
130
|
|
|
131
|
+
// Seed runtime counters from DB so they reflect prior usage
|
|
132
|
+
if (sessionDB) {
|
|
133
|
+
try {
|
|
134
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
135
|
+
counters.sandboxRuns = allTime.allSandboxRuns;
|
|
136
|
+
counters.searchQueries = allTime.allSearchQueries;
|
|
137
|
+
} catch {
|
|
138
|
+
// Non-fatal: counter seeding from DB failed
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
142
|
// Reset runtime stats for new session
|
|
143
143
|
runtimeStats.bytesReturned = {};
|
|
144
144
|
runtimeStats.bytesIndexed = 0;
|
|
@@ -154,7 +154,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
154
154
|
if (sessionDB) {
|
|
155
155
|
registerCompactorTools(pi, {
|
|
156
156
|
sessionDB,
|
|
157
|
-
contentStore,
|
|
158
157
|
getSessionId: () => currentSessionId,
|
|
159
158
|
getBlocks: () => cachedBlocks,
|
|
160
159
|
getCounters,
|
|
@@ -165,7 +164,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
165
164
|
registerCommands(pi, getCommandDeps());
|
|
166
165
|
|
|
167
166
|
// Register info-screen group
|
|
168
|
-
const infoRegistry =
|
|
167
|
+
const infoRegistry = globalThis.__unipi_info_registry;
|
|
169
168
|
if (infoRegistry && sessionDB) {
|
|
170
169
|
const sdb = sessionDB;
|
|
171
170
|
const sid = () => currentSessionId;
|
|
@@ -188,7 +187,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
188
187
|
dataProvider: async () => {
|
|
189
188
|
try {
|
|
190
189
|
const { getInfoScreenData } = await import("./info-screen.js");
|
|
191
|
-
const data = await getInfoScreenData(sdb, sid(),
|
|
190
|
+
const data = await getInfoScreenData(sdb, sid(), getCounters());
|
|
192
191
|
return {
|
|
193
192
|
tokensSaved: { value: data.tokensSaved.value, detail: data.tokensSaved.detail },
|
|
194
193
|
costSaved: { value: data.costSaved.value, detail: data.costSaved.detail },
|
|
@@ -204,7 +203,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
204
203
|
});
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
emitEvent(pi
|
|
206
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
208
207
|
name: MODULES.COMPACTOR,
|
|
209
208
|
version: "0.1.0",
|
|
210
209
|
commands: Object.values(COMPACTOR_COMMANDS),
|
|
@@ -213,10 +212,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
213
212
|
|
|
214
213
|
debug("MODULE_READY", { commands: Object.values(COMPACTOR_COMMANDS), tools: Object.values(COMPACTOR_TOOLS) });
|
|
215
214
|
|
|
216
|
-
if (config.fts5Index.mode === "auto" && contentStore) {
|
|
217
|
-
// TODO: index project files
|
|
218
|
-
}
|
|
219
|
-
|
|
220
215
|
ctx.ui.notify("🗜️ Compactor ready", "info");
|
|
221
216
|
});
|
|
222
217
|
|
|
@@ -232,7 +227,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
232
227
|
const { join } = await import("node:path");
|
|
233
228
|
const strategies: Array<{ key: string; config: CompactorStrategyConfig }> = [
|
|
234
229
|
{ key: "commits", config: config.commits },
|
|
235
|
-
{ key: "fts5Index", config: config.fts5Index },
|
|
236
230
|
];
|
|
237
231
|
for (const { key, config: strat } of strategies) {
|
|
238
232
|
if ((strat as any).autoDetect === "git") {
|
|
@@ -283,7 +277,9 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
283
277
|
|
|
284
278
|
pi.on("session_before_compact", async (event, _ctx) => {
|
|
285
279
|
if (sessionDB) {
|
|
286
|
-
|
|
280
|
+
// Use closure currentSessionId — Pi's session_before_compact event
|
|
281
|
+
// does not include sessionId at the top level.
|
|
282
|
+
const sessionId = currentSessionId;
|
|
287
283
|
const events = sessionDB.getEvents(sessionId, { limit: 1000 });
|
|
288
284
|
const stats = sessionDB.getSessionStats(sessionId);
|
|
289
285
|
debug("session_before_compact", { sessionId, eventCount: events.length, compactCount: stats?.compact_count ?? 0 });
|
|
@@ -297,44 +293,55 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
297
293
|
|
|
298
294
|
pi.on("session_compact", async (event, _ctx) => {
|
|
299
295
|
if (sessionDB) {
|
|
300
|
-
|
|
296
|
+
// Use closure currentSessionId — Pi's session_compact event does not
|
|
297
|
+
// include sessionId at the top level (it's inside compactionEntry).
|
|
298
|
+
const sessionId = currentSessionId;
|
|
301
299
|
sessionDB.incrementCompactCount(sessionId);
|
|
302
300
|
counters.compactions++;
|
|
303
301
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
//
|
|
302
|
+
// Pi's session_compact event structure: { compactionEntry, fromExtension }
|
|
303
|
+
// tokensBefore is inside compactionEntry, not at event root.
|
|
304
|
+
const compactionEntry = (event as any).compactionEntry;
|
|
305
|
+
const tokensBefore = compactionEntry?.tokensBefore ?? 0;
|
|
306
|
+
|
|
307
|
+
if (tokensBefore > 0) {
|
|
308
|
+
// Use actual token count from Pi's compactionEntry.
|
|
309
|
+
// Compaction typically keeps ~10-15% of original context.
|
|
312
310
|
const charsBefore = tokensBefore * 4;
|
|
313
|
-
|
|
314
|
-
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
|
|
311
|
+
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.12);
|
|
315
312
|
const charsKept = tokensAfter * 4;
|
|
316
313
|
const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
|
|
317
314
|
counters.totalTokensCompacted += tokensBefore - tokensAfter;
|
|
318
315
|
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
|
|
319
|
-
} else
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
316
|
+
} else {
|
|
317
|
+
// tokensBefore unavailable — use session event count as a rough heuristic.
|
|
318
|
+
// This happens when Pi's compaction doesn't report tokensBefore.
|
|
319
|
+
// We estimate from the number of events stored in this session.
|
|
320
|
+
try {
|
|
321
|
+
const eventCount = sessionDB.getEventCount(sessionId);
|
|
322
|
+
if (eventCount > 0) {
|
|
323
|
+
// Rough estimate: ~500 tokens per event on average
|
|
324
|
+
const estTokensBefore = eventCount * 500;
|
|
325
|
+
const estTokensAfter = Math.round(estTokensBefore * 0.12);
|
|
326
|
+
const charsBefore = estTokensBefore * 4;
|
|
327
|
+
const charsKept = estTokensAfter * 4;
|
|
328
|
+
counters.totalTokensCompacted += estTokensBefore - estTokensAfter;
|
|
329
|
+
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, eventCount);
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Non-fatal: heuristic estimation failed
|
|
333
|
+
}
|
|
324
334
|
}
|
|
325
|
-
debug("session_compact", { sessionId, tokensBefore,
|
|
335
|
+
debug("session_compact", { sessionId, tokensBefore, hasCompactionEntry: !!compactionEntry });
|
|
326
336
|
}
|
|
327
337
|
});
|
|
328
338
|
|
|
329
339
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
330
340
|
debug("session_shutdown");
|
|
331
|
-
// WAL checkpoint: TRUNCATE on shutdown to keep DB file size down
|
|
332
|
-
contentStore?.checkpointWAL("TRUNCATE");
|
|
333
341
|
if (sessionDB) {
|
|
334
342
|
sessionDB.cleanupOldSessions(7);
|
|
335
343
|
}
|
|
336
344
|
executor?.cleanupBackgrounded();
|
|
337
|
-
contentStore?.close();
|
|
338
345
|
sessionDB?.close();
|
|
339
346
|
});
|
|
340
347
|
|
|
@@ -412,7 +419,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
412
419
|
|
|
413
420
|
pi.on("tool_result", async (event, _ctx) => {
|
|
414
421
|
if (!sessionDB) return;
|
|
415
|
-
|
|
422
|
+
// Use closure currentSessionId — tool_result events use the same session
|
|
423
|
+
const sessionId = currentSessionId;
|
|
416
424
|
const toolNameRaw = (event as any).toolName ?? "";
|
|
417
425
|
const isError = (event as any).isError ?? false;
|
|
418
426
|
|