@pi-unipi/compactor 0.2.3 → 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 +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/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,19 +99,6 @@ 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
|
|
|
@@ -127,7 +112,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
127
112
|
// Commands registered inside session_start after init() when deps are ready
|
|
128
113
|
const getCommandDeps = () => ({
|
|
129
114
|
sessionDB,
|
|
130
|
-
contentStore,
|
|
131
115
|
getSessionId: () => currentSessionId,
|
|
132
116
|
getBlocks: () => cachedBlocks,
|
|
133
117
|
getCounters,
|
|
@@ -144,6 +128,17 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
144
128
|
|
|
145
129
|
debug("session_start", { sessionId: fullSessionId, projectDir });
|
|
146
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
|
+
|
|
147
142
|
// Reset runtime stats for new session
|
|
148
143
|
runtimeStats.bytesReturned = {};
|
|
149
144
|
runtimeStats.bytesIndexed = 0;
|
|
@@ -159,7 +154,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
159
154
|
if (sessionDB) {
|
|
160
155
|
registerCompactorTools(pi, {
|
|
161
156
|
sessionDB,
|
|
162
|
-
contentStore,
|
|
163
157
|
getSessionId: () => currentSessionId,
|
|
164
158
|
getBlocks: () => cachedBlocks,
|
|
165
159
|
getCounters,
|
|
@@ -170,7 +164,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
170
164
|
registerCommands(pi, getCommandDeps());
|
|
171
165
|
|
|
172
166
|
// Register info-screen group
|
|
173
|
-
const infoRegistry =
|
|
167
|
+
const infoRegistry = globalThis.__unipi_info_registry;
|
|
174
168
|
if (infoRegistry && sessionDB) {
|
|
175
169
|
const sdb = sessionDB;
|
|
176
170
|
const sid = () => currentSessionId;
|
|
@@ -193,7 +187,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
193
187
|
dataProvider: async () => {
|
|
194
188
|
try {
|
|
195
189
|
const { getInfoScreenData } = await import("./info-screen.js");
|
|
196
|
-
const data = await getInfoScreenData(sdb, sid(),
|
|
190
|
+
const data = await getInfoScreenData(sdb, sid(), getCounters());
|
|
197
191
|
return {
|
|
198
192
|
tokensSaved: { value: data.tokensSaved.value, detail: data.tokensSaved.detail },
|
|
199
193
|
costSaved: { value: data.costSaved.value, detail: data.costSaved.detail },
|
|
@@ -209,7 +203,7 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
209
203
|
});
|
|
210
204
|
}
|
|
211
205
|
|
|
212
|
-
emitEvent(pi
|
|
206
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
213
207
|
name: MODULES.COMPACTOR,
|
|
214
208
|
version: "0.1.0",
|
|
215
209
|
commands: Object.values(COMPACTOR_COMMANDS),
|
|
@@ -218,10 +212,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
218
212
|
|
|
219
213
|
debug("MODULE_READY", { commands: Object.values(COMPACTOR_COMMANDS), tools: Object.values(COMPACTOR_TOOLS) });
|
|
220
214
|
|
|
221
|
-
if (config.fts5Index.mode === "auto" && contentStore) {
|
|
222
|
-
// TODO: index project files
|
|
223
|
-
}
|
|
224
|
-
|
|
225
215
|
ctx.ui.notify("🗜️ Compactor ready", "info");
|
|
226
216
|
});
|
|
227
217
|
|
|
@@ -237,7 +227,6 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
237
227
|
const { join } = await import("node:path");
|
|
238
228
|
const strategies: Array<{ key: string; config: CompactorStrategyConfig }> = [
|
|
239
229
|
{ key: "commits", config: config.commits },
|
|
240
|
-
{ key: "fts5Index", config: config.fts5Index },
|
|
241
230
|
];
|
|
242
231
|
for (const { key, config: strat } of strategies) {
|
|
243
232
|
if ((strat as any).autoDetect === "git") {
|
|
@@ -315,44 +304,44 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
315
304
|
const compactionEntry = (event as any).compactionEntry;
|
|
316
305
|
const tokensBefore = compactionEntry?.tokensBefore ?? 0;
|
|
317
306
|
|
|
318
|
-
// Use actual runtimeStats for byte measurement instead of heuristic
|
|
319
|
-
const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce((s, b) => s + b, 0);
|
|
320
|
-
const totalBytesProcessed = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed + totalBytesReturned;
|
|
321
|
-
|
|
322
307
|
if (tokensBefore > 0) {
|
|
323
|
-
// Use actual token count from Pi's compactionEntry
|
|
308
|
+
// Use actual token count from Pi's compactionEntry.
|
|
309
|
+
// Compaction typically keeps ~10-15% of original context.
|
|
324
310
|
const charsBefore = tokensBefore * 4;
|
|
325
|
-
|
|
326
|
-
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
|
|
311
|
+
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.12);
|
|
327
312
|
const charsKept = tokensAfter * 4;
|
|
328
313
|
const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
|
|
329
314
|
counters.totalTokensCompacted += tokensBefore - tokensAfter;
|
|
330
315
|
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
|
|
331
316
|
} else {
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
341
333
|
}
|
|
342
334
|
}
|
|
343
|
-
debug("session_compact", { sessionId, tokensBefore,
|
|
335
|
+
debug("session_compact", { sessionId, tokensBefore, hasCompactionEntry: !!compactionEntry });
|
|
344
336
|
}
|
|
345
337
|
});
|
|
346
338
|
|
|
347
339
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
348
340
|
debug("session_shutdown");
|
|
349
|
-
// WAL checkpoint: TRUNCATE on shutdown to keep DB file size down
|
|
350
|
-
contentStore?.checkpointWAL("TRUNCATE");
|
|
351
341
|
if (sessionDB) {
|
|
352
342
|
sessionDB.cleanupOldSessions(7);
|
|
353
343
|
}
|
|
354
344
|
executor?.cleanupBackgrounded();
|
|
355
|
-
contentStore?.close();
|
|
356
345
|
sessionDB?.close();
|
|
357
346
|
});
|
|
358
347
|
|
package/src/info-screen.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Info-screen integration for @pi-unipi/compactor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Stats driven by COMPACTION SAVINGS (the compactor's actual value),
|
|
5
|
+
* not sandbox/index diversion bytes.
|
|
6
|
+
*
|
|
7
|
+
* Data sources (in priority order):
|
|
8
|
+
* 1. Runtime counters (in-memory, current session only)
|
|
9
|
+
* 2. DB compaction stats (total_chars_before/kept in session_meta)
|
|
10
|
+
* 3. Session event counts (session_events table, always reliable)
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import type { SessionDB } from "./session/db.js";
|
|
9
|
-
import type { RuntimeStats, FullReport } from "./session/analytics.js";
|
|
10
|
-
import { AnalyticsEngine, createMinimalDb } from "./session/analytics.js";
|
|
11
14
|
import { getLastCompactionStats } from "./compaction/hooks.js";
|
|
12
15
|
import { parseUsageStats } from "@pi-unipi/info-screen/usage-parser.js";
|
|
16
|
+
import type { RuntimeCounters } from "./types.js";
|
|
13
17
|
|
|
14
18
|
export interface CompactorInfoData {
|
|
15
19
|
tokensSaved: { value: string; detail: string };
|
|
@@ -45,7 +49,6 @@ function estimateCostPerToken(): number | null {
|
|
|
45
49
|
const models = usage.byModelToday;
|
|
46
50
|
const todayKeys = Object.keys(models);
|
|
47
51
|
if (todayKeys.length > 0) {
|
|
48
|
-
// Pick the model with most tokens today
|
|
49
52
|
const topModel = todayKeys.reduce((a, b) => models[a].tokens > models[b].tokens ? a : b);
|
|
50
53
|
const entry = models[topModel];
|
|
51
54
|
if (entry.tokens > 0 && entry.cost > 0) {
|
|
@@ -70,40 +73,92 @@ function estimateCostPerToken(): number | null {
|
|
|
70
73
|
export async function getInfoScreenData(
|
|
71
74
|
sessionDB: SessionDB,
|
|
72
75
|
sessionId: string,
|
|
73
|
-
|
|
76
|
+
counters?: RuntimeCounters,
|
|
74
77
|
): Promise<CompactorInfoData> {
|
|
75
78
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// ── Compaction savings (the compactor's actual value) ──
|
|
80
|
+
// Priority: in-memory counter → DB per-session stats → DB all-time stats
|
|
81
|
+
let tokensSaved = counters?.totalTokensCompacted ?? 0;
|
|
82
|
+
let charsBefore = 0;
|
|
83
|
+
let charsKept = 0;
|
|
84
|
+
|
|
85
|
+
if (tokensSaved === 0) {
|
|
86
|
+
// Try DB per-session stats
|
|
87
|
+
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
88
|
+
if (sessionStats) {
|
|
89
|
+
charsBefore = (sessionStats as any).total_chars_before ?? 0;
|
|
90
|
+
charsKept = (sessionStats as any).total_chars_kept ?? 0;
|
|
91
|
+
tokensSaved = Math.round((charsBefore - charsKept) / 4);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if (tokensSaved === 0) {
|
|
96
|
+
// Try DB all-time stats
|
|
97
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
98
|
+
charsBefore = allTime.allCharsBefore;
|
|
99
|
+
charsKept = allTime.allCharsKept;
|
|
100
|
+
tokensSaved = Math.round((charsBefore - charsKept) / 4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Compaction count ──
|
|
104
|
+
let compactionCount = counters?.compactions ?? 0;
|
|
105
|
+
if (compactionCount === 0) {
|
|
106
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
107
|
+
compactionCount = allTime.allCompactions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Compression ratio / pct reduction ──
|
|
111
|
+
let pctReduction = 0;
|
|
112
|
+
if (charsBefore > 0) {
|
|
113
|
+
pctReduction = Math.round((1 - charsKept / charsBefore) * 100);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Tool call counts from session_events (always reliable) ──
|
|
117
|
+
// The session_events table captures every tool_result event, so this
|
|
118
|
+
// is an accurate count regardless of runtimeStats state.
|
|
119
|
+
interface ToolCountRow { category: string; cnt: number }
|
|
120
|
+
let toolCountRows: ToolCountRow[] = [];
|
|
121
|
+
let totalToolCalls = 0;
|
|
122
|
+
try {
|
|
123
|
+
const db = sessionDB.getDb();
|
|
124
|
+
if (db) {
|
|
125
|
+
toolCountRows = db.prepare(
|
|
126
|
+
"SELECT category, COUNT(*) as cnt FROM session_events WHERE session_id = ? GROUP BY category",
|
|
127
|
+
).all(sessionId) as ToolCountRow[];
|
|
128
|
+
for (const row of toolCountRows) {
|
|
129
|
+
totalToolCalls += row.cnt;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Non-fatal: DB query failed, show zero
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Build per-tool breakdown for display
|
|
137
|
+
const toolBreakdown = toolCountRows.length > 0
|
|
138
|
+
? toolCountRows
|
|
139
|
+
.sort((a, b) => b.cnt - a.cnt)
|
|
140
|
+
.map(r => ` ${r.category.padEnd(20)} ${String(r.cnt).padStart(5)} events`)
|
|
141
|
+
.join("\n")
|
|
93
142
|
: "No tool calls yet";
|
|
94
143
|
|
|
95
|
-
|
|
144
|
+
const topCategory = toolCountRows.length > 0
|
|
145
|
+
? toolCountRows.reduce((a, b) => a.cnt > b.cnt ? a : b)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const top5Detail = toolCountRows.length > 0
|
|
149
|
+
? toolCountRows
|
|
150
|
+
.sort((a, b) => b.cnt - a.cnt)
|
|
151
|
+
.slice(0, 5)
|
|
152
|
+
.map(r => `${r.category}: ${r.cnt} events`)
|
|
153
|
+
.join("\n")
|
|
154
|
+
: "No tool calls yet";
|
|
155
|
+
|
|
156
|
+
// ── Cost saved estimate ──
|
|
96
157
|
const costPerToken = estimateCostPerToken();
|
|
97
158
|
const costSaved = costPerToken !== null ? tokensSaved * costPerToken : null;
|
|
98
159
|
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
const top5Tools = toolsWithCalls.slice(0, 5);
|
|
102
|
-
const top5Detail = top5Tools.length > 0
|
|
103
|
-
? top5Tools.map(t =>
|
|
104
|
-
`${t.tool}: ${formatTokens(t.tokens)} (${t.calls} calls)`
|
|
105
|
-
).join("\n")
|
|
106
|
-
: "No tool calls yet";
|
|
160
|
+
// ── Last compaction details ──
|
|
161
|
+
const compactStats = getLastCompactionStats();
|
|
107
162
|
|
|
108
163
|
return {
|
|
109
164
|
tokensSaved: {
|
|
@@ -117,24 +172,26 @@ export async function getInfoScreenData(
|
|
|
117
172
|
: "Cost data unavailable for current model",
|
|
118
173
|
},
|
|
119
174
|
pctReduction: {
|
|
120
|
-
value: `${
|
|
121
|
-
detail:
|
|
175
|
+
value: `${pctReduction}%`,
|
|
176
|
+
detail: charsBefore > 0
|
|
177
|
+
? `${formatTokens(Math.round(charsBefore / 4))} before → ${formatTokens(Math.round(charsKept / 4))} after compaction`
|
|
178
|
+
: "No compaction data yet",
|
|
122
179
|
},
|
|
123
180
|
topTools: {
|
|
124
|
-
value:
|
|
181
|
+
value: topCategory ? `${topCategory.category}: ${topCategory.cnt}` : "N/A",
|
|
125
182
|
detail: top5Detail,
|
|
126
183
|
},
|
|
127
184
|
compactions: {
|
|
128
|
-
value: String(
|
|
185
|
+
value: String(compactionCount),
|
|
129
186
|
detail: compactStats
|
|
130
|
-
? `Last: ${compactStats.
|
|
131
|
-
:
|
|
132
|
-
? `${
|
|
187
|
+
? `Last: ${compactStats.totalMessages} messages (~${formatTokens(compactStats.tokensBefore)} tokens) → ${compactStats.kept} messages (~${formatTokens(compactStats.tokensAfterEst)} tokens)`
|
|
188
|
+
: compactionCount > 0
|
|
189
|
+
? `${compactionCount} compaction(s) across all sessions`
|
|
133
190
|
: "No compactions yet",
|
|
134
191
|
},
|
|
135
192
|
toolCalls: {
|
|
136
|
-
value: String(
|
|
137
|
-
detail: `${
|
|
193
|
+
value: String(totalToolCalls),
|
|
194
|
+
detail: `${totalToolCalls} events across ${toolCountRows.length} categor${toolCountRows.length !== 1 ? "ies" : "y"}`,
|
|
138
195
|
},
|
|
139
196
|
};
|
|
140
197
|
} catch {
|
package/src/session/db.ts
CHANGED
|
@@ -59,9 +59,9 @@ async function getSQLite() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
interface PreparedStatement {
|
|
62
|
-
get(...args:
|
|
63
|
-
all(...args:
|
|
64
|
-
run(...args:
|
|
62
|
+
get(...args: unknown[]): unknown;
|
|
63
|
+
all(...args: unknown[]): unknown[];
|
|
64
|
+
run(...args: unknown[]): { changes: number };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const MAX_EVENTS_PER_SESSION = 1000;
|
|
@@ -143,8 +143,8 @@ export class SessionDB {
|
|
|
143
143
|
const safeAddColumn = (table: string, col: string, def: string) => {
|
|
144
144
|
try {
|
|
145
145
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`);
|
|
146
|
-
} catch (e:
|
|
147
|
-
if (e
|
|
146
|
+
} catch (e: unknown) {
|
|
147
|
+
if (e instanceof Error && e.message.includes("duplicate column")) return;
|
|
148
148
|
throw e;
|
|
149
149
|
}
|
|
150
150
|
};
|
|
@@ -156,6 +156,21 @@ export class SessionDB {
|
|
|
156
156
|
safeAddColumn("session_events", "data_hash", "TEXT NOT NULL DEFAULT ''");
|
|
157
157
|
this.db.pragma("user_version = 1");
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
if (currentVersion < 2) {
|
|
161
|
+
// V2: Add sandbox/search counter columns for persistent stats
|
|
162
|
+
const safeAddColumn = (table: string, col: string, def: string) => {
|
|
163
|
+
try {
|
|
164
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`);
|
|
165
|
+
} catch (e: unknown) {
|
|
166
|
+
if (e instanceof Error && e.message.includes("duplicate column")) return;
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
safeAddColumn("session_meta", "sandbox_runs", "INTEGER NOT NULL DEFAULT 0");
|
|
171
|
+
safeAddColumn("session_meta", "search_queries", "INTEGER NOT NULL DEFAULT 0");
|
|
172
|
+
this.db.pragma("user_version = 2");
|
|
173
|
+
}
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
private prepareStatements(): void {
|
|
@@ -171,10 +186,12 @@ export class SessionDB {
|
|
|
171
186
|
p("evictLowestPriority", `DELETE FROM session_events WHERE id = (SELECT id FROM session_events WHERE session_id = ? ORDER BY priority ASC, id ASC LIMIT 1)`);
|
|
172
187
|
p("updateMetaLastEvent", `UPDATE session_meta SET last_event_at = datetime('now'), event_count = event_count + 1 WHERE session_id = ?`);
|
|
173
188
|
p("ensureSession", `INSERT OR IGNORE INTO session_meta (session_id, project_dir) VALUES (?, ?)`);
|
|
174
|
-
p("getSessionStats", `SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count, total_chars_before, total_chars_kept, total_messages_summarized FROM session_meta WHERE session_id = ?`);
|
|
189
|
+
p("getSessionStats", `SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count, total_chars_before, total_chars_kept, total_messages_summarized, sandbox_runs, search_queries FROM session_meta WHERE session_id = ?`);
|
|
175
190
|
p("incrementCompactCount", `UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?`);
|
|
176
191
|
p("addCompactionStats", `UPDATE session_meta SET total_chars_before = total_chars_before + ?, total_chars_kept = total_chars_kept + ?, total_messages_summarized = total_messages_summarized + ? WHERE session_id = ?`);
|
|
177
|
-
p("getAllTimeStats", `SELECT COALESCE(SUM(total_chars_before), 0) AS all_chars_before, COALESCE(SUM(total_chars_kept), 0) AS all_chars_kept, COALESCE(SUM(total_messages_summarized), 0) AS all_messages_summarized, COALESCE(SUM(compact_count), 0) AS all_compactions FROM session_meta`);
|
|
192
|
+
p("getAllTimeStats", `SELECT COALESCE(SUM(total_chars_before), 0) AS all_chars_before, COALESCE(SUM(total_chars_kept), 0) AS all_chars_kept, COALESCE(SUM(total_messages_summarized), 0) AS all_messages_summarized, COALESCE(SUM(compact_count), 0) AS all_compactions, COALESCE(SUM(sandbox_runs), 0) AS all_sandbox_runs, COALESCE(SUM(search_queries), 0) AS all_search_queries FROM session_meta`);
|
|
193
|
+
p("incrementSandboxRuns", `UPDATE session_meta SET sandbox_runs = sandbox_runs + 1 WHERE session_id = ?`);
|
|
194
|
+
p("incrementSearchQueries", `UPDATE session_meta SET search_queries = search_queries + 1 WHERE session_id = ?`);
|
|
178
195
|
p("upsertResume", `INSERT INTO session_resume (session_id, snapshot, event_count) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET snapshot = excluded.snapshot, event_count = excluded.event_count, created_at = datetime('now'), consumed = 0`);
|
|
179
196
|
p("getResume", `SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?`);
|
|
180
197
|
p("markResumeConsumed", `UPDATE session_resume SET consumed = 1 WHERE session_id = ?`);
|
|
@@ -248,17 +265,29 @@ export class SessionDB {
|
|
|
248
265
|
this.stmt("addCompactionStats").run(charsBefore, charsKept, messagesSummarized, sessionId);
|
|
249
266
|
}
|
|
250
267
|
|
|
251
|
-
getAllTimeStats(): { allCharsBefore: number; allCharsKept: number; allMessagesSummarized: number; allCompactions: number } {
|
|
252
|
-
if (!this.stmts) return { allCharsBefore: 0, allCharsKept: 0, allMessagesSummarized: 0, allCompactions: 0 };
|
|
253
|
-
const row = this.stmt("getAllTimeStats").get() as { all_chars_before: number; all_chars_kept: number; all_messages_summarized: number; all_compactions: number };
|
|
268
|
+
getAllTimeStats(): { allCharsBefore: number; allCharsKept: number; allMessagesSummarized: number; allCompactions: number; allSandboxRuns: number; allSearchQueries: number } {
|
|
269
|
+
if (!this.stmts) return { allCharsBefore: 0, allCharsKept: 0, allMessagesSummarized: 0, allCompactions: 0, allSandboxRuns: 0, allSearchQueries: 0 };
|
|
270
|
+
const row = this.stmt("getAllTimeStats").get() as { all_chars_before: number; all_chars_kept: number; all_messages_summarized: number; all_compactions: number; all_sandbox_runs: number; all_search_queries: number };
|
|
254
271
|
return {
|
|
255
272
|
allCharsBefore: row?.all_chars_before ?? 0,
|
|
256
273
|
allCharsKept: row?.all_chars_kept ?? 0,
|
|
257
274
|
allMessagesSummarized: row?.all_messages_summarized ?? 0,
|
|
258
275
|
allCompactions: row?.all_compactions ?? 0,
|
|
276
|
+
allSandboxRuns: row?.all_sandbox_runs ?? 0,
|
|
277
|
+
allSearchQueries: row?.all_search_queries ?? 0,
|
|
259
278
|
};
|
|
260
279
|
}
|
|
261
280
|
|
|
281
|
+
incrementSandboxRuns(sessionId: string): void {
|
|
282
|
+
if (!this.stmts) return;
|
|
283
|
+
this.stmt("incrementSandboxRuns").run(sessionId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
incrementSearchQueries(sessionId: string): void {
|
|
287
|
+
if (!this.stmts) return;
|
|
288
|
+
this.stmt("incrementSearchQueries").run(sessionId);
|
|
289
|
+
}
|
|
290
|
+
|
|
262
291
|
upsertResume(sessionId: string, snapshot: string, eventCount?: number): void {
|
|
263
292
|
if (!this.stmts) return;
|
|
264
293
|
this.stmt("upsertResume").run(sessionId, snapshot, eventCount ?? 0);
|
|
@@ -294,7 +323,7 @@ export class SessionDB {
|
|
|
294
323
|
}
|
|
295
324
|
|
|
296
325
|
/** Expose the underlying db for AnalyticsEngine (read-only queries). Returns null if init failed. */
|
|
297
|
-
getDb():
|
|
326
|
+
getDb(): { prepare(sql: string): { get(...args: unknown[]): unknown; all(...args: unknown[]): unknown[]; run(...args: unknown[]): { changes: number } }; exec(sql: string): void; close(): void } | null { return this.db as ReturnType<typeof this.getDb> ?? null; }
|
|
298
327
|
|
|
299
328
|
close(): void {
|
|
300
329
|
try { this.db.close(); } catch { /* ignore */ }
|
package/src/session/extract.ts
CHANGED
|
@@ -73,6 +73,43 @@ export function extractEventsFromToolResult(result: ToolResult): SessionEvent[]
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Sandbox execution
|
|
77
|
+
if (["sandbox", "ctx_execute", "sandbox_file", "ctx_execute_file", "sandbox_batch", "ctx_batch_execute"].includes(result.toolName)) {
|
|
78
|
+
const language = String(result.toolInput.language ?? "unknown");
|
|
79
|
+
const code = String(result.toolInput.code ?? "").slice(0, 200);
|
|
80
|
+
events.push({
|
|
81
|
+
type: "sandbox_execution",
|
|
82
|
+
category: "sandbox",
|
|
83
|
+
data: `[${language}] ${code}`,
|
|
84
|
+
priority: EventPriority.LOW,
|
|
85
|
+
data_hash: "",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Content search queries
|
|
90
|
+
if (["content_search", "ctx_search"].includes(result.toolName)) {
|
|
91
|
+
const query = String(result.toolInput.query ?? "").slice(0, 200);
|
|
92
|
+
events.push({
|
|
93
|
+
type: "content_search",
|
|
94
|
+
category: "search",
|
|
95
|
+
data: query,
|
|
96
|
+
priority: EventPriority.LOW,
|
|
97
|
+
data_hash: "",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Content indexing
|
|
102
|
+
if (["content_index", "ctx_index", "content_fetch", "ctx_fetch_and_index"].includes(result.toolName)) {
|
|
103
|
+
const label = String(result.toolInput.label ?? result.toolInput.url ?? "").slice(0, 200);
|
|
104
|
+
events.push({
|
|
105
|
+
type: "content_indexed",
|
|
106
|
+
category: "index",
|
|
107
|
+
data: label,
|
|
108
|
+
priority: EventPriority.LOW,
|
|
109
|
+
data_hash: "",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
return events;
|
|
77
114
|
}
|
|
78
115
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ctx_batch_execute tool — atomic batch of commands
|
|
2
|
+
* ctx_batch_execute tool — atomic batch of commands
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { PolyglotExecutor } from "../executor/executor.js";
|
|
6
|
-
import type {
|
|
7
|
-
import type { Language, ExecResult, SearchResult } from "../types.js";
|
|
6
|
+
import type { Language, ExecResult } from "../types.js";
|
|
8
7
|
|
|
9
8
|
export interface BatchCommand {
|
|
10
9
|
type: "execute";
|
|
@@ -13,22 +12,15 @@ export interface BatchCommand {
|
|
|
13
12
|
timeout?: number;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export
|
|
17
|
-
type: "search";
|
|
18
|
-
query: string;
|
|
19
|
-
limit?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type BatchItem = BatchCommand | BatchSearch;
|
|
15
|
+
export type BatchItem = BatchCommand;
|
|
23
16
|
|
|
24
17
|
export interface BatchResult {
|
|
25
18
|
results: Array<
|
|
26
|
-
|
|
27
|
-
| { type: "search"; results: SearchResult[] }
|
|
19
|
+
{ type: "execute"; result: ExecResult }
|
|
28
20
|
>;
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
export async function ctxBatchExecute(
|
|
23
|
+
export async function ctxBatchExecute(items: BatchItem[]): Promise<BatchResult> {
|
|
32
24
|
const results: BatchResult["results"] = [];
|
|
33
25
|
const executor = new PolyglotExecutor();
|
|
34
26
|
|
|
@@ -40,9 +32,6 @@ export async function ctxBatchExecute(store: ContentStore, items: BatchItem[]):
|
|
|
40
32
|
timeout: item.timeout ?? 30000,
|
|
41
33
|
});
|
|
42
34
|
results.push({ type: "execute", result });
|
|
43
|
-
} else {
|
|
44
|
-
const searchResults = await store.search(item.query, { limit: item.limit ?? 10 });
|
|
45
|
-
results.push({ type: "search", results: searchResults });
|
|
46
35
|
}
|
|
47
36
|
}
|
|
48
37
|
|
package/src/tools/ctx-doctor.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { COMPACTOR_CONFIG_PATH } from "../config/manager.js";
|
|
7
7
|
import type { SessionDB } from "../session/db.js";
|
|
8
|
-
import type { ContentStore } from "../store/index.js";
|
|
9
8
|
|
|
10
9
|
export interface DoctorResult {
|
|
11
10
|
healthy: boolean;
|
|
@@ -18,7 +17,6 @@ export interface DoctorResult {
|
|
|
18
17
|
|
|
19
18
|
export async function ctxDoctor(
|
|
20
19
|
sessionDB: SessionDB,
|
|
21
|
-
contentStore: ContentStore,
|
|
22
20
|
): Promise<DoctorResult> {
|
|
23
21
|
const checks: DoctorResult["checks"] = [];
|
|
24
22
|
|
|
@@ -45,22 +43,6 @@ export async function ctxDoctor(
|
|
|
45
43
|
});
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
// Content store check
|
|
49
|
-
try {
|
|
50
|
-
const stats = await contentStore.getStats();
|
|
51
|
-
checks.push({
|
|
52
|
-
name: "Content Store",
|
|
53
|
-
status: "pass",
|
|
54
|
-
message: `FTS5 index: ${stats.sources} sources, ${stats.chunks} chunks`,
|
|
55
|
-
});
|
|
56
|
-
} catch (err) {
|
|
57
|
-
checks.push({
|
|
58
|
-
name: "Content Store",
|
|
59
|
-
status: "fail",
|
|
60
|
-
message: `FTS5 error: ${err}`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
46
|
// Runtime checks
|
|
65
47
|
const runtimes = ["node", "python3", "bash"];
|
|
66
48
|
for (const rt of runtimes) {
|