@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.
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Info-screen integration for @pi-unipi/compactor
3
3
  *
4
- * Budget-focused stats: tokensSaved, costSaved, pctReduction,
5
- * topTools, compactions, toolCalls.
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
- runtimeStats: RuntimeStats,
76
+ counters?: RuntimeCounters,
74
77
  ): Promise<CompactorInfoData> {
75
78
  try {
76
- const db = sessionDB.getDb();
77
- const adapter = db ?? createMinimalDb();
78
- const engine = new AnalyticsEngine(adapter);
79
- const report = engine.queryAll(runtimeStats);
80
- const compactStats = getLastCompactionStats();
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
- // Tokens saved: bytes kept out of context / 4
83
- const tokensSaved = Math.round(report.savings.kept_out / 4);
84
-
85
- // Per-tool breakdown table for tokensSaved detail
86
- const toolsWithCalls = report.savings.by_tool
87
- .filter(t => t.calls > 0)
88
- .sort((a, b) => b.tokens - a.tokens);
89
- const toolBreakdown = toolsWithCalls.length > 0
90
- ? toolsWithCalls.map(t =>
91
- ` ${t.tool.padEnd(20)} ${String(t.calls).padStart(4)} calls ${formatTokens(t.tokens).padStart(8)} tok`
92
- ).join("\n")
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
- // Cost saved: tokensSaved × cost per token
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
- // Top consuming tool
100
- const topTool = toolsWithCalls[0];
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: `${report.savings.pct}%`,
121
- detail: `${formatTokens(Math.round(report.savings.processed_kb * 1024 / 4))} processed → ${formatTokens(Math.round(report.savings.entered_kb * 1024 / 4))} entered context`,
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: topTool ? `${topTool.tool}: ${formatTokens(topTool.tokens)}` : "N/A",
181
+ value: topCategory ? `${topCategory.category}: ${topCategory.cnt}` : "N/A",
125
182
  detail: top5Detail,
126
183
  },
127
184
  compactions: {
128
- value: String(report.continuity.compact_count),
185
+ value: String(compactionCount),
129
186
  detail: compactStats
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`
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(report.savings.total_calls),
137
- detail: `${report.savings.total_calls} total tool calls across ${toolsWithCalls.length} tool${toolsWithCalls.length !== 1 ? "s" : ""}`,
193
+ value: String(totalToolCalls),
194
+ detail: `${totalToolCalls} events across ${toolCountRows.length} categor${toolCountRows.length !== 1 ? "ies" : "y"}`,
138
195
  },
139
196
  };
140
197
  } catch {
@@ -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: any[]): any;
63
- all(...args: any[]): any[];
64
- run(...args: any[]): { changes: number };
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: any) {
147
- if (e?.message?.includes("duplicate column")) return;
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(): any { return this.db ?? null; }
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 */ }
@@ -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 + searches
2
+ * ctx_batch_execute tool — atomic batch of commands
3
3
  */
4
4
 
5
5
  import { PolyglotExecutor } from "../executor/executor.js";
6
- import type { ContentStore } from "../store/index.js";
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 interface BatchSearch {
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
- | { type: "execute"; result: ExecResult }
27
- | { type: "search"; results: SearchResult[] }
19
+ { type: "execute"; result: ExecResult }
28
20
  >;
29
21
  }
30
22
 
31
- export async function ctxBatchExecute(store: ContentStore, items: BatchItem[]): Promise<BatchResult> {
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
 
@@ -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) {
@@ -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
- const storeStats = await contentStore.getStats();
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: counters?.compactions ?? sessionStats?.compact_count ?? 0,
32
- tokensSaved: counters?.totalTokensCompacted ?? 0,
33
- compressionRatio: "N/A",
34
- indexedDocs: storeStats.sources,
35
- indexedChunks: storeStats.chunks,
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
+ }