@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/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/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/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) {
|
package/src/tools/ctx-stats.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ctx_stats tool — context savings dashboard
|
|
3
|
+
*
|
|
4
|
+
* Stats driven by compaction savings (DB-first) with runtime counter fallback.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { SessionDB } from "../session/db.js";
|
|
6
|
-
import type { ContentStore } from "../store/index.js";
|
|
7
8
|
import type { RuntimeCounters } from "../types.js";
|
|
8
9
|
|
|
9
10
|
export interface CtxStatsResult {
|
|
@@ -11,29 +12,85 @@ export interface CtxStatsResult {
|
|
|
11
12
|
compactions: number;
|
|
12
13
|
tokensSaved: number;
|
|
13
14
|
compressionRatio: string;
|
|
14
|
-
indexedDocs: number;
|
|
15
|
-
indexedChunks: number;
|
|
16
15
|
sandboxRuns: number;
|
|
17
16
|
searchQueries: number;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export async function ctxStats(
|
|
21
20
|
sessionDB: SessionDB,
|
|
22
|
-
contentStore: ContentStore,
|
|
23
21
|
sessionId: string,
|
|
24
22
|
counters?: RuntimeCounters,
|
|
25
23
|
): Promise<CtxStatsResult> {
|
|
26
24
|
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
// Compute tokensSaved: prefer in-memory counters (current session),
|
|
27
|
+
// fall back to per-session DB stats, then all-time DB stats.
|
|
28
|
+
let tokensSaved = counters?.totalTokensCompacted ?? 0;
|
|
29
|
+
if (tokensSaved === 0 && sessionStats) {
|
|
30
|
+
const sessionCharsBefore = (sessionStats as any).total_chars_before ?? 0;
|
|
31
|
+
const sessionCharsKept = (sessionStats as any).total_chars_kept ?? 0;
|
|
32
|
+
tokensSaved = Math.round((sessionCharsBefore - sessionCharsKept) / 4);
|
|
33
|
+
}
|
|
34
|
+
if (tokensSaved === 0) {
|
|
35
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
36
|
+
tokensSaved = Math.round((allTime.allCharsBefore - allTime.allCharsKept) / 4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Compute compactions: prefer in-memory counter (current session),
|
|
40
|
+
// fall back to per-session DB, then all-time DB.
|
|
41
|
+
let compactions = counters?.compactions ?? 0;
|
|
42
|
+
if (compactions === 0) {
|
|
43
|
+
compactions = sessionStats?.compact_count ?? 0;
|
|
44
|
+
}
|
|
45
|
+
if (compactions === 0) {
|
|
46
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
47
|
+
compactions = allTime.allCompactions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Compression ratio
|
|
51
|
+
let ratio = "N/A";
|
|
52
|
+
if (sessionStats) {
|
|
53
|
+
const before = (sessionStats as any).total_chars_before ?? 0;
|
|
54
|
+
const kept = (sessionStats as any).total_chars_kept ?? 0;
|
|
55
|
+
if (before > 0 && kept > 0) {
|
|
56
|
+
ratio = `${(before / kept).toFixed(1)}:1`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
28
59
|
|
|
29
60
|
return {
|
|
30
61
|
sessionEvents: sessionStats?.event_count ?? 0,
|
|
31
|
-
compactions
|
|
32
|
-
tokensSaved
|
|
33
|
-
compressionRatio:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sandboxRuns: counters?.sandboxRuns ?? 0,
|
|
37
|
-
searchQueries: counters?.searchQueries ?? 0,
|
|
62
|
+
compactions,
|
|
63
|
+
tokensSaved,
|
|
64
|
+
compressionRatio: ratio,
|
|
65
|
+
sandboxRuns: computeSandboxRuns(counters, sessionDB, sessionId),
|
|
66
|
+
searchQueries: computeSearchQueries(counters, sessionDB, sessionId),
|
|
38
67
|
};
|
|
39
68
|
}
|
|
69
|
+
|
|
70
|
+
/** Compute sandbox runs: prefer in-memory counter, fall back to DB. */
|
|
71
|
+
function computeSandboxRuns(counters: RuntimeCounters | undefined, sessionDB: SessionDB, sessionId: string): number {
|
|
72
|
+
let sandboxRuns = counters?.sandboxRuns ?? 0;
|
|
73
|
+
if (sandboxRuns === 0) {
|
|
74
|
+
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
75
|
+
sandboxRuns = (sessionStats as any)?.sandbox_runs ?? 0;
|
|
76
|
+
}
|
|
77
|
+
if (sandboxRuns === 0) {
|
|
78
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
79
|
+
sandboxRuns = allTime.allSandboxRuns;
|
|
80
|
+
}
|
|
81
|
+
return sandboxRuns;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Compute search queries: prefer in-memory counter, fall back to DB. */
|
|
85
|
+
function computeSearchQueries(counters: RuntimeCounters | undefined, sessionDB: SessionDB, sessionId: string): number {
|
|
86
|
+
let searchQueries = counters?.searchQueries ?? 0;
|
|
87
|
+
if (searchQueries === 0) {
|
|
88
|
+
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
89
|
+
searchQueries = (sessionStats as any)?.search_queries ?? 0;
|
|
90
|
+
}
|
|
91
|
+
if (searchQueries === 0) {
|
|
92
|
+
const allTime = sessionDB.getAllTimeStats();
|
|
93
|
+
searchQueries = allTime.allSearchQueries;
|
|
94
|
+
}
|
|
95
|
+
return searchQueries;
|
|
96
|
+
}
|