@pi-unipi/compactor 0.2.1 → 0.2.2
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/package.json +3 -2
- package/src/commands/index.ts +2 -2
- package/src/compaction/hooks.ts +3 -5
- package/src/config/schema.ts +1 -1
- package/src/display/diff-presentation.ts +6 -1
- package/src/display/diff-renderer.ts +34 -8
- package/src/display/diff-width-safety.ts +83 -0
- package/src/display/line-width-safety.ts +14 -2
- package/src/index.ts +131 -23
- package/src/info-screen.ts +136 -51
- package/src/session/analytics.ts +198 -0
- package/src/session/db.ts +3 -0
- package/src/tools/register.ts +2 -2
- package/src/types.ts +5 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, FTS5 search, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@pi-unipi/core": "*"
|
|
41
|
+
"@pi-unipi/core": "*",
|
|
42
|
+
"@pi-unipi/info-screen": "*"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@mariozechner/pi-coding-agent": "*",
|
package/src/commands/index.ts
CHANGED
|
@@ -34,8 +34,8 @@ export interface CommandDeps {
|
|
|
34
34
|
getCounters?: () => RuntimeCounters;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function deprecationLog(
|
|
38
|
-
|
|
37
|
+
function deprecationLog(_oldName: string, _newName: string): void {
|
|
38
|
+
// Deprecation logging disabled — was writing to stdout causing TUI rendering issues.
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
package/src/compaction/hooks.ts
CHANGED
|
@@ -21,11 +21,9 @@ const formatTokens = (n: number): string => {
|
|
|
21
21
|
return String(n);
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
const dbg = (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const details = data ? " " + JSON.stringify(data) : "";
|
|
28
|
-
console.error(`[compactor:${ts}] ${event}${details}`);
|
|
24
|
+
const dbg = (_debug: boolean, _event: string, _data?: Record<string, unknown>) => {
|
|
25
|
+
// Debug logging disabled — was writing to stdout causing TUI rendering issues.
|
|
26
|
+
return;
|
|
29
27
|
};
|
|
30
28
|
|
|
31
29
|
const previewContent = (content: unknown): string => {
|
package/src/config/schema.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { DiffLayout } from "../types.js";
|
|
6
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
6
7
|
|
|
7
8
|
export function selectDiffLayout(
|
|
8
9
|
terminalWidth: number,
|
|
@@ -12,9 +13,13 @@ export function selectDiffLayout(
|
|
|
12
13
|
return terminalWidth >= 100 ? "split" : "unified";
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Clamp text to maxWidth visible columns, ANSI-aware */
|
|
15
17
|
export function clampWidth(text: string, maxWidth: number): string {
|
|
16
18
|
return text
|
|
17
19
|
.split("\n")
|
|
18
|
-
.map((line) =>
|
|
20
|
+
.map((line) => {
|
|
21
|
+
if (visibleWidth(line) <= maxWidth) return line;
|
|
22
|
+
return truncateToWidth(line, maxWidth, "…");
|
|
23
|
+
})
|
|
19
24
|
.join("\n");
|
|
20
25
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* syntax highlighting, and Nerd Font detection
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
7
|
+
|
|
6
8
|
export type DiffLayout = "auto" | "split" | "unified";
|
|
7
9
|
export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
|
|
8
10
|
|
|
@@ -150,16 +152,28 @@ function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
|
|
|
150
152
|
return type === "add" ? "+ " : type === "remove" ? "- " : " ";
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function renderUnified(
|
|
155
|
+
function renderUnified(
|
|
156
|
+
diff: DiffLine[],
|
|
157
|
+
indicator: DiffIndicator,
|
|
158
|
+
maxWidth?: number,
|
|
159
|
+
): string {
|
|
154
160
|
return diff.map((line) => {
|
|
155
161
|
const prefix = indicator === "bars"
|
|
156
162
|
? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
|
|
157
163
|
: indicatorChar(line.type, indicator);
|
|
158
|
-
|
|
164
|
+
const rendered = prefix + line.text;
|
|
165
|
+
if (maxWidth && visibleWidth(rendered) > maxWidth) {
|
|
166
|
+
return truncateToWidth(rendered, maxWidth, "…");
|
|
167
|
+
}
|
|
168
|
+
return rendered;
|
|
159
169
|
}).join("\n");
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
function renderSplit(
|
|
172
|
+
function renderSplit(
|
|
173
|
+
diff: DiffLine[],
|
|
174
|
+
indicator: DiffIndicator,
|
|
175
|
+
maxW?: number,
|
|
176
|
+
): string {
|
|
163
177
|
const left: string[] = [];
|
|
164
178
|
const right: string[] = [];
|
|
165
179
|
|
|
@@ -176,12 +190,24 @@ function renderSplit(diff: DiffLine[], indicator: DiffIndicator): string {
|
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
const
|
|
193
|
+
const halfW = maxW ? Math.floor(maxW / 2) - 2 : 40;
|
|
194
|
+
const colW = Math.max(
|
|
195
|
+
...left.map((l) => visibleWidth(l)),
|
|
196
|
+
...right.map((l) => visibleWidth(l)),
|
|
197
|
+
Math.min(halfW, 40),
|
|
198
|
+
);
|
|
180
199
|
const result: string[] = [];
|
|
181
200
|
for (let i = 0; i < left.length; i++) {
|
|
182
|
-
const
|
|
201
|
+
const lTrunc = visibleWidth(left[i]) > colW
|
|
202
|
+
? truncateToWidth(left[i], colW, "…")
|
|
203
|
+
: left[i].padEnd(colW);
|
|
183
204
|
const sep = left[i] && right[i] ? " │ " : " ";
|
|
184
|
-
|
|
205
|
+
let rLine = right[i];
|
|
206
|
+
if (maxW && visibleWidth(lTrunc + sep + rLine) > maxW) {
|
|
207
|
+
const rBudget = maxW - visibleWidth(lTrunc + sep);
|
|
208
|
+
rLine = truncateToWidth(rLine, Math.max(1, rBudget), "…");
|
|
209
|
+
}
|
|
210
|
+
result.push(lTrunc + sep + rLine);
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
return result.join("\n");
|
|
@@ -229,10 +255,10 @@ export function renderDiff(
|
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
if (effectiveLayout === "split") {
|
|
232
|
-
return renderSplit(highlightedDiff, effectiveIndicator);
|
|
258
|
+
return renderSplit(highlightedDiff, effectiveIndicator, maxWidth);
|
|
233
259
|
}
|
|
234
260
|
|
|
235
|
-
return renderUnified(highlightedDiff, effectiveIndicator);
|
|
261
|
+
return renderUnified(highlightedDiff, effectiveIndicator, maxWidth);
|
|
236
262
|
}
|
|
237
263
|
|
|
238
264
|
export function renderEditDiffResult(
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff width safety — truncate diff lines to terminal width
|
|
3
|
+
*
|
|
4
|
+
* Pi's renderDiff() in diff.js produces lines without width
|
|
5
|
+
* truncation. When a diff line's visible content exceeds the
|
|
6
|
+
* terminal width, the TUI crashes with:
|
|
7
|
+
* "Rendered line N exceeds terminal width (X > Y)"
|
|
8
|
+
*
|
|
9
|
+
* This module provides clampDiffToWidth() which truncates
|
|
10
|
+
* each diff line to a safe width, preventing TUI crashes.
|
|
11
|
+
*
|
|
12
|
+
* The diff format from pi's generateDiffString() is:
|
|
13
|
+
* [+/-/ ]LINE_NUM CONTENT
|
|
14
|
+
* e.g.: "+ 38 │ The root cause is a **compound failure**..."
|
|
15
|
+
*
|
|
16
|
+
* We detect the terminal width from process.stdout and clamp
|
|
17
|
+
* each line, accounting for the rendering overhead of the
|
|
18
|
+
* edit tool's Box nesting (approx 4-6 chars of padding).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
22
|
+
|
|
23
|
+
/** Rendering overhead from Box nesting in edit tool components */
|
|
24
|
+
const RENDER_OVERHEAD = 6;
|
|
25
|
+
|
|
26
|
+
/** Minimum useful line width (don't clamp below this) */
|
|
27
|
+
const MIN_LINE_WIDTH = 20;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the terminal width with fallback.
|
|
31
|
+
* Uses process.stdout.columns (updated on resize).
|
|
32
|
+
*/
|
|
33
|
+
function getTerminalWidth(): number {
|
|
34
|
+
return process.stdout?.columns ?? 80;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a diff string so every line fits within terminal width.
|
|
39
|
+
* Preserves the diff prefix (+/-/ ) and line number while
|
|
40
|
+
* truncating the content portion.
|
|
41
|
+
*
|
|
42
|
+
* @param diff - The diff string from generateDiffString()
|
|
43
|
+
* @param maxWidth - Override for terminal width (for testing)
|
|
44
|
+
* @returns The clamped diff string (may be same reference if no clamping needed)
|
|
45
|
+
*/
|
|
46
|
+
export function clampDiffToWidth(
|
|
47
|
+
diff: string,
|
|
48
|
+
maxWidth?: number,
|
|
49
|
+
): string {
|
|
50
|
+
const termW = maxWidth ?? getTerminalWidth();
|
|
51
|
+
const safeW = Math.max(MIN_LINE_WIDTH, termW - RENDER_OVERHEAD);
|
|
52
|
+
|
|
53
|
+
const lines = diff.split("\n");
|
|
54
|
+
let changed = false;
|
|
55
|
+
|
|
56
|
+
const result = lines.map((line) => {
|
|
57
|
+
const vw = visibleWidth(line);
|
|
58
|
+
if (vw <= safeW) return line;
|
|
59
|
+
|
|
60
|
+
changed = true;
|
|
61
|
+
|
|
62
|
+
// Try to preserve the diff prefix (+/-/ ) and line number
|
|
63
|
+
// Format: [+/-/ ]LINE_NUM CONTENT
|
|
64
|
+
// The prefix and line number are critical for readability
|
|
65
|
+
const prefixMatch = line.match(/^([+\- ])\s*(\d*)\s/);
|
|
66
|
+
if (prefixMatch) {
|
|
67
|
+
const prefixLen = prefixMatch[0].length;
|
|
68
|
+
// Calculate how much content we can keep
|
|
69
|
+
const contentBudget = safeW - prefixLen;
|
|
70
|
+
if (contentBudget >= MIN_LINE_WIDTH) {
|
|
71
|
+
const prefix = line.slice(0, prefixLen);
|
|
72
|
+
const content = line.slice(prefixLen);
|
|
73
|
+
const truncated = truncateToWidth(content, contentBudget, "…");
|
|
74
|
+
return prefix + truncated;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fallback: truncate the entire line
|
|
79
|
+
return truncateToWidth(line, safeW, "…");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return changed ? result.join("\n") : diff;
|
|
83
|
+
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Line width safety — width clamping with collapsed hints
|
|
3
|
+
*
|
|
4
|
+
* Uses ANSI-aware visibleWidth measurement from pi-tui to properly
|
|
5
|
+
* handle lines containing escape codes. Falls back to raw-length
|
|
6
|
+
* measurement when pi-tui is unavailable.
|
|
3
7
|
*/
|
|
4
8
|
|
|
9
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clamp each line to maxWidth visible columns.
|
|
13
|
+
* Uses pi-tui's visibleWidth() for ANSI-aware measurement and
|
|
14
|
+
* truncateToWidth() for ANSI-safe truncation.
|
|
15
|
+
*/
|
|
5
16
|
export function clampLineWidth(lines: string[], maxWidth: number): string[] {
|
|
6
17
|
return lines.map((line) => {
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
const vw = visibleWidth(line);
|
|
19
|
+
if (vw <= maxWidth) return line;
|
|
20
|
+
return truncateToWidth(line, maxWidth, "…");
|
|
9
21
|
});
|
|
10
22
|
}
|
|
11
23
|
|
package/src/index.ts
CHANGED
|
@@ -16,17 +16,45 @@ import { registerCompactorTools } from "./tools/register.js";
|
|
|
16
16
|
import { normalizeMessages } from "./compaction/normalize.js";
|
|
17
17
|
import { filterNoise } from "./compaction/filter-noise.js";
|
|
18
18
|
import type { NormalizedBlock, CompactorStrategyConfig, RuntimeCounters } from "./types.js";
|
|
19
|
+
import type { RuntimeStats } from "./session/analytics.js";
|
|
19
20
|
|
|
20
21
|
/** Debug logger — only logs when config.debug === true */
|
|
21
22
|
function createDebugLogger(getConfig: () => { debug: boolean }) {
|
|
22
|
-
return (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const details = data ? " " + JSON.stringify(data) : "";
|
|
26
|
-
console.error(`[compactor:${ts}] ${event}${details}`);
|
|
23
|
+
return (_event: string, _data?: Record<string, unknown>) => {
|
|
24
|
+
// Debug logging disabled — was writing to stdout causing TUI rendering issues.
|
|
25
|
+
return;
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
/** Measure byte size of a tool_result event's response content. */
|
|
30
|
+
function measureResponseBytes(event: any): number {
|
|
31
|
+
try {
|
|
32
|
+
const content = event.content;
|
|
33
|
+
if (typeof content === "string") return Buffer.byteLength(content, "utf-8");
|
|
34
|
+
if (Array.isArray(content)) {
|
|
35
|
+
return content.reduce((sum: number, block: any) => {
|
|
36
|
+
if (typeof block?.text === "string") return sum + Buffer.byteLength(block.text, "utf-8");
|
|
37
|
+
if (typeof block === "string") return sum + Buffer.byteLength(block, "utf-8");
|
|
38
|
+
return sum;
|
|
39
|
+
}, 0);
|
|
40
|
+
}
|
|
41
|
+
if (event.output && typeof event.output === "string") return Buffer.byteLength(event.output, "utf-8");
|
|
42
|
+
} catch {
|
|
43
|
+
// Non-blocking: byte measurement errors silently skipped
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Check if a tool is a sandbox tool (output stays in sandbox, not context). */
|
|
49
|
+
function isSandboxTool(name: string): boolean {
|
|
50
|
+
return name === "bash" || name === "Bash";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check if a tool is an index tool (content goes to FTS5, not context). Future-proofing. */
|
|
54
|
+
function isIndexTool(_name: string): boolean {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
export default function compactorExtension(pi: ExtensionAPI): void {
|
|
31
59
|
let sessionDB: SessionDB | null = null;
|
|
32
60
|
let contentStore: ContentStore | null = null;
|
|
@@ -43,6 +71,16 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
43
71
|
};
|
|
44
72
|
const getCounters = () => counters;
|
|
45
73
|
|
|
74
|
+
const runtimeStats: RuntimeStats = {
|
|
75
|
+
bytesReturned: {},
|
|
76
|
+
bytesIndexed: 0,
|
|
77
|
+
bytesSandboxed: 0,
|
|
78
|
+
calls: {},
|
|
79
|
+
sessionStart: Date.now(),
|
|
80
|
+
cacheHits: 0,
|
|
81
|
+
cacheBytesSaved: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
46
84
|
const debug = createDebugLogger(() => config);
|
|
47
85
|
|
|
48
86
|
const init = async () => {
|
|
@@ -58,8 +96,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
58
96
|
const db = new SessionDB();
|
|
59
97
|
await db.init();
|
|
60
98
|
sessionDB = db;
|
|
61
|
-
} catch
|
|
62
|
-
|
|
99
|
+
} catch {
|
|
100
|
+
// Silently ignore — SessionDB init failure is handled gracefully.
|
|
63
101
|
sessionDB = null;
|
|
64
102
|
}
|
|
65
103
|
|
|
@@ -70,8 +108,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
70
108
|
const cs = new ContentStore();
|
|
71
109
|
await cs.init();
|
|
72
110
|
contentStore = cs;
|
|
73
|
-
} catch
|
|
74
|
-
|
|
111
|
+
} catch {
|
|
112
|
+
// Silently ignore — ContentStore init failure is handled gracefully.
|
|
75
113
|
contentStore = null;
|
|
76
114
|
}
|
|
77
115
|
}
|
|
@@ -101,6 +139,15 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
101
139
|
|
|
102
140
|
debug("session_start", { sessionId: fullSessionId, projectDir });
|
|
103
141
|
|
|
142
|
+
// Reset runtime stats for new session
|
|
143
|
+
runtimeStats.bytesReturned = {};
|
|
144
|
+
runtimeStats.bytesIndexed = 0;
|
|
145
|
+
runtimeStats.bytesSandboxed = 0;
|
|
146
|
+
runtimeStats.calls = {};
|
|
147
|
+
runtimeStats.sessionStart = Date.now();
|
|
148
|
+
runtimeStats.cacheHits = 0;
|
|
149
|
+
runtimeStats.cacheBytesSaved = 0;
|
|
150
|
+
|
|
104
151
|
sessionDB?.ensureSession(fullSessionId, projectDir);
|
|
105
152
|
|
|
106
153
|
// Register all compactor tools with Pi (deps now have live sessionDB)
|
|
@@ -119,9 +166,8 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
119
166
|
|
|
120
167
|
// Register info-screen group
|
|
121
168
|
const infoRegistry = (globalThis as any).__unipi_info_registry;
|
|
122
|
-
if (infoRegistry && sessionDB
|
|
169
|
+
if (infoRegistry && sessionDB) {
|
|
123
170
|
const sdb = sessionDB;
|
|
124
|
-
const cs = contentStore;
|
|
125
171
|
const sid = () => currentSessionId;
|
|
126
172
|
infoRegistry.registerGroup({
|
|
127
173
|
id: "compactor",
|
|
@@ -131,23 +177,25 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
131
177
|
config: {
|
|
132
178
|
showByDefault: true,
|
|
133
179
|
stats: [
|
|
134
|
-
{ id: "
|
|
180
|
+
{ id: "tokensSaved", label: "Tokens saved", show: true },
|
|
181
|
+
{ id: "costSaved", label: "Cost saved", show: true },
|
|
182
|
+
{ id: "pctReduction", label: "% Reduction", show: true },
|
|
183
|
+
{ id: "topTools", label: "Top tools", show: true },
|
|
135
184
|
{ id: "compactions", label: "Compactions", show: true },
|
|
136
|
-
{ id: "
|
|
137
|
-
{ id: "compressionRatio", label: "Compression ratio", show: true },
|
|
138
|
-
{ id: "indexedDocs", label: "Indexed docs", show: true },
|
|
185
|
+
{ id: "toolCalls", label: "Tool calls", show: true },
|
|
139
186
|
],
|
|
140
187
|
},
|
|
141
188
|
dataProvider: async () => {
|
|
142
189
|
try {
|
|
143
190
|
const { getInfoScreenData } = await import("./info-screen.js");
|
|
144
|
-
const data = await getInfoScreenData(sdb,
|
|
191
|
+
const data = await getInfoScreenData(sdb, sid(), runtimeStats);
|
|
145
192
|
return {
|
|
146
|
-
sessionEvents: { value: data.sessionEvents.value, detail: data.sessionEvents.detail },
|
|
147
|
-
compactions: { value: data.compactions.value, detail: data.compactions.detail },
|
|
148
193
|
tokensSaved: { value: data.tokensSaved.value, detail: data.tokensSaved.detail },
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
costSaved: { value: data.costSaved.value, detail: data.costSaved.detail },
|
|
195
|
+
pctReduction: { value: data.pctReduction.value, detail: data.pctReduction.detail },
|
|
196
|
+
topTools: { value: data.topTools.value, detail: data.topTools.detail },
|
|
197
|
+
compactions: { value: data.compactions.value, detail: data.compactions.detail },
|
|
198
|
+
toolCalls: { value: data.toolCalls.value, detail: data.toolCalls.detail },
|
|
151
199
|
};
|
|
152
200
|
} catch {
|
|
153
201
|
return {};
|
|
@@ -252,11 +300,29 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
252
300
|
const sessionId = `${(event as any).sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
253
301
|
sessionDB.incrementCompactCount(sessionId);
|
|
254
302
|
counters.compactions++;
|
|
303
|
+
|
|
304
|
+
// Use actual runtimeStats for byte measurement instead of heuristic
|
|
305
|
+
const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce((s, b) => s + b, 0);
|
|
306
|
+
const totalBytesProcessed = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed + totalBytesReturned;
|
|
307
|
+
// charsBefore = total bytes processed by all tools (proxy for context window usage)
|
|
308
|
+
// charsKept = bytes that stayed in context (bytesReturned, minus what compaction removed)
|
|
255
309
|
const tokensBefore = (event as any).tokensBefore ?? 0;
|
|
256
|
-
if (tokensBefore > 0) {
|
|
257
|
-
|
|
310
|
+
if (totalBytesProcessed > 0 && tokensBefore > 0) {
|
|
311
|
+
// Use actual token count from Pi, estimate chars from it
|
|
312
|
+
const charsBefore = tokensBefore * 4;
|
|
313
|
+
// Estimate kept chars: proportional to what remains after compaction
|
|
314
|
+
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
|
|
315
|
+
const charsKept = tokensAfter * 4;
|
|
316
|
+
const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
|
|
317
|
+
counters.totalTokensCompacted += tokensBefore - tokensAfter;
|
|
318
|
+
sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
|
|
319
|
+
} else if (tokensBefore > 0) {
|
|
320
|
+
// Fallback: only tokensBefore available, use conservative estimate
|
|
321
|
+
const tokensAfter = (event as any).tokensAfter ?? Math.round(tokensBefore * 0.15);
|
|
322
|
+
counters.totalTokensCompacted += tokensBefore - tokensAfter;
|
|
323
|
+
sessionDB.addCompactionStats(sessionId, tokensBefore * 4, tokensAfter * 4, 1);
|
|
258
324
|
}
|
|
259
|
-
debug("session_compact", { sessionId });
|
|
325
|
+
debug("session_compact", { sessionId, tokensBefore, totalBytesProcessed });
|
|
260
326
|
}
|
|
261
327
|
});
|
|
262
328
|
|
|
@@ -365,6 +431,24 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
365
431
|
debug("event_stored", { category: ev.category, type: ev.type });
|
|
366
432
|
}
|
|
367
433
|
|
|
434
|
+
// Track byte consumption per tool for analytics
|
|
435
|
+
try {
|
|
436
|
+
const responseBytes = measureResponseBytes(event);
|
|
437
|
+
if (responseBytes > 0) {
|
|
438
|
+
const tName = (event as any).toolName ?? "unknown";
|
|
439
|
+
runtimeStats.calls[tName] = (runtimeStats.calls[tName] || 0) + 1;
|
|
440
|
+
runtimeStats.bytesReturned[tName] = (runtimeStats.bytesReturned[tName] || 0) + responseBytes;
|
|
441
|
+
if (isSandboxTool(tName)) {
|
|
442
|
+
runtimeStats.bytesSandboxed += responseBytes;
|
|
443
|
+
}
|
|
444
|
+
if (isIndexTool(tName)) {
|
|
445
|
+
runtimeStats.bytesIndexed += responseBytes;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
// Non-blocking: byte tracking errors silently skipped
|
|
450
|
+
}
|
|
451
|
+
|
|
368
452
|
// Apply display overrides for built-in tools
|
|
369
453
|
const toolName = (event as any).toolName ?? "";
|
|
370
454
|
const td = config.toolDisplay;
|
|
@@ -385,6 +469,30 @@ export default function compactorExtension(pi: ExtensionAPI): void {
|
|
|
385
469
|
} catch {
|
|
386
470
|
// Non-fatal: display override failed
|
|
387
471
|
}
|
|
472
|
+
|
|
473
|
+
// Width-safe diff truncation for edit/write tool results.
|
|
474
|
+
// Pi's renderDiff() does not truncate lines to terminal width,
|
|
475
|
+
// causing TUI crashes on narrow terminals. We truncate the
|
|
476
|
+
// diff string in details.diff before it reaches the TUI.
|
|
477
|
+
const diffToolNames = ["edit", "Edit", "write", "Write"];
|
|
478
|
+
if (diffToolNames.includes(toolName)) {
|
|
479
|
+
try {
|
|
480
|
+
const details = (event as any).details as
|
|
481
|
+
{ diff?: string } | undefined;
|
|
482
|
+
if (details?.diff) {
|
|
483
|
+
const { clampDiffToWidth } = await import(
|
|
484
|
+
"./display/diff-width-safety.js"
|
|
485
|
+
);
|
|
486
|
+
const clamped = clampDiffToWidth(details.diff);
|
|
487
|
+
if (clamped !== details.diff) {
|
|
488
|
+
debug("diff_width_clamped", { toolName });
|
|
489
|
+
return { details: { ...details, diff: clamped } } as any;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (err) {
|
|
493
|
+
debug("diff_width_clamp_error", { error: String(err) });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
388
496
|
});
|
|
389
497
|
|
|
390
498
|
pi.on("message_update", async (event, _ctx) => {
|
package/src/info-screen.ts
CHANGED
|
@@ -1,66 +1,151 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Info-screen integration for @pi-unipi/compactor
|
|
3
|
+
*
|
|
4
|
+
* Budget-focused stats: tokensSaved, costSaved, pctReduction,
|
|
5
|
+
* topTools, compactions, toolCalls.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import type { SessionDB } from "./session/db.js";
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
9
|
+
import type { RuntimeStats, FullReport } from "./session/analytics.js";
|
|
10
|
+
import { AnalyticsEngine, createMinimalDb } from "./session/analytics.js";
|
|
8
11
|
import { getLastCompactionStats } from "./compaction/hooks.js";
|
|
12
|
+
import { parseUsageStats } from "@pi-unipi/info-screen/usage-parser.js";
|
|
9
13
|
|
|
10
|
-
export interface
|
|
11
|
-
sessionEvents: { value: string; detail: string };
|
|
12
|
-
compactions: { value: string; detail: string };
|
|
14
|
+
export interface CompactorInfoData {
|
|
13
15
|
tokensSaved: { value: string; detail: string };
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
costSaved: { value: string; detail: string };
|
|
17
|
+
pctReduction: { value: string; detail: string };
|
|
18
|
+
topTools: { value: string; detail: string };
|
|
19
|
+
compactions: { value: string; detail: string };
|
|
20
|
+
toolCalls: { value: string; detail: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Format token count for display (e.g., "12.4k", "1.2M"). */
|
|
24
|
+
function formatTokens(n: number): string {
|
|
25
|
+
if (n < 1000) return String(n);
|
|
26
|
+
if (n < 10_000) return `${(n / 1000).toFixed(1)}k`;
|
|
27
|
+
if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
|
|
28
|
+
if (n < 10_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
29
|
+
return `${Math.round(n / 1_000_000)}M`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Format cost for display (e.g., "$0.34", "<$0.01"). */
|
|
33
|
+
function formatCost(n: number): string {
|
|
34
|
+
if (n === 0) return "$0.00";
|
|
35
|
+
if (n < 0.01) return "<$0.01";
|
|
36
|
+
if (n < 1) return `$${n.toFixed(2)}`;
|
|
37
|
+
return `$${n.toFixed(2)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Estimate cost per token for the most-used model in the current session. */
|
|
41
|
+
function estimateCostPerToken(): number | null {
|
|
42
|
+
try {
|
|
43
|
+
const usage = parseUsageStats();
|
|
44
|
+
// Use today's most-used model if available, otherwise all-time
|
|
45
|
+
const models = usage.byModelToday;
|
|
46
|
+
const todayKeys = Object.keys(models);
|
|
47
|
+
if (todayKeys.length > 0) {
|
|
48
|
+
// Pick the model with most tokens today
|
|
49
|
+
const topModel = todayKeys.reduce((a, b) => models[a].tokens > models[b].tokens ? a : b);
|
|
50
|
+
const entry = models[topModel];
|
|
51
|
+
if (entry.tokens > 0 && entry.cost > 0) {
|
|
52
|
+
return entry.cost / entry.tokens;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Fall back to all-time model data
|
|
56
|
+
const allKeys = Object.keys(usage.byModel);
|
|
57
|
+
if (allKeys.length > 0) {
|
|
58
|
+
const topModel = allKeys.reduce((a, b) => usage.byModel[a].tokens > usage.byModel[b].tokens ? a : b);
|
|
59
|
+
const entry = usage.byModel[topModel];
|
|
60
|
+
if (entry.tokens > 0 && entry.cost > 0) {
|
|
61
|
+
return entry.cost / entry.tokens;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
18
68
|
}
|
|
19
69
|
|
|
20
70
|
export async function getInfoScreenData(
|
|
21
71
|
sessionDB: SessionDB,
|
|
22
|
-
contentStore: ContentStore,
|
|
23
72
|
sessionId: string,
|
|
24
|
-
|
|
25
|
-
): Promise<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
73
|
+
runtimeStats: RuntimeStats,
|
|
74
|
+
): Promise<CompactorInfoData> {
|
|
75
|
+
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();
|
|
81
|
+
|
|
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")
|
|
93
|
+
: "No tool calls yet";
|
|
94
|
+
|
|
95
|
+
// Cost saved: tokensSaved × cost per token
|
|
96
|
+
const costPerToken = estimateCostPerToken();
|
|
97
|
+
const costSaved = costPerToken !== null ? tokensSaved * costPerToken : null;
|
|
98
|
+
|
|
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";
|
|
29
107
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
return {
|
|
109
|
+
tokensSaved: {
|
|
110
|
+
value: formatTokens(tokensSaved),
|
|
111
|
+
detail: toolBreakdown,
|
|
112
|
+
},
|
|
113
|
+
costSaved: {
|
|
114
|
+
value: costSaved !== null ? formatCost(costSaved) : "N/A",
|
|
115
|
+
detail: costSaved !== null
|
|
116
|
+
? `~${formatTokens(tokensSaved)} tokens × $${(costPerToken! * 1_000_000).toFixed(2)}/M tokens`
|
|
117
|
+
: "Cost data unavailable for current model",
|
|
118
|
+
},
|
|
119
|
+
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`,
|
|
122
|
+
},
|
|
123
|
+
topTools: {
|
|
124
|
+
value: topTool ? `${topTool.tool}: ${formatTokens(topTool.tokens)}` : "N/A",
|
|
125
|
+
detail: top5Detail,
|
|
126
|
+
},
|
|
127
|
+
compactions: {
|
|
128
|
+
value: String(report.continuity.compact_count),
|
|
129
|
+
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`
|
|
133
|
+
: "No compactions yet",
|
|
134
|
+
},
|
|
135
|
+
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" : ""}`,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
// Never throw from dataProvider — return zeroed stats
|
|
142
|
+
return {
|
|
143
|
+
tokensSaved: { value: "0", detail: "No data" },
|
|
144
|
+
costSaved: { value: "N/A", detail: "No data" },
|
|
145
|
+
pctReduction: { value: "0%", detail: "No data" },
|
|
146
|
+
topTools: { value: "N/A", detail: "No data" },
|
|
147
|
+
compactions: { value: "0", detail: "No data" },
|
|
148
|
+
toolCalls: { value: "0", detail: "No data" },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
66
151
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsEngine — Runtime savings + session continuity reporting.
|
|
3
|
+
*
|
|
4
|
+
* Ported from context-mode's AnalyticsEngine, trimmed to budget-focused stats.
|
|
5
|
+
* Omits: formatReport(), categoryLabels, categoryHints, ThinkInCodeComparison,
|
|
6
|
+
* SandboxIO, dataBar(), visual formatting helpers.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const engine = new AnalyticsEngine(sessionDb);
|
|
10
|
+
* const report = engine.queryAll(runtimeStats);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────
|
|
14
|
+
// Types
|
|
15
|
+
// ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Database adapter — anything with a prepare() method (better-sqlite3, bun:sqlite, etc.) */
|
|
18
|
+
export interface DatabaseAdapter {
|
|
19
|
+
prepare(sql: string): {
|
|
20
|
+
run(...params: unknown[]): unknown;
|
|
21
|
+
get(...params: unknown[]): unknown;
|
|
22
|
+
all(...params: unknown[]): unknown[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Context savings result */
|
|
27
|
+
export interface ContextSavings {
|
|
28
|
+
rawBytes: number;
|
|
29
|
+
contextBytes: number;
|
|
30
|
+
savedBytes: number;
|
|
31
|
+
savedPercent: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Runtime stats tracked during a live session. */
|
|
35
|
+
export interface RuntimeStats {
|
|
36
|
+
bytesReturned: Record<string, number>;
|
|
37
|
+
bytesIndexed: number;
|
|
38
|
+
bytesSandboxed: number;
|
|
39
|
+
calls: Record<string, number>;
|
|
40
|
+
sessionStart: number;
|
|
41
|
+
cacheHits: number;
|
|
42
|
+
cacheBytesSaved: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Unified report combining runtime stats, DB analytics, and continuity data. */
|
|
46
|
+
export interface FullReport {
|
|
47
|
+
/** Runtime context savings */
|
|
48
|
+
savings: {
|
|
49
|
+
processed_kb: number;
|
|
50
|
+
entered_kb: number;
|
|
51
|
+
saved_kb: number;
|
|
52
|
+
pct: number;
|
|
53
|
+
savings_ratio: number;
|
|
54
|
+
by_tool: Array<{ tool: string; calls: number; context_kb: number; tokens: number }>;
|
|
55
|
+
total_calls: number;
|
|
56
|
+
total_bytes_returned: number;
|
|
57
|
+
kept_out: number;
|
|
58
|
+
total_processed: number;
|
|
59
|
+
};
|
|
60
|
+
/** Session metadata from SessionDB */
|
|
61
|
+
session: {
|
|
62
|
+
id: string;
|
|
63
|
+
uptime_min: string;
|
|
64
|
+
};
|
|
65
|
+
/** Session continuity data */
|
|
66
|
+
continuity: {
|
|
67
|
+
total_events: number;
|
|
68
|
+
compact_count: number;
|
|
69
|
+
resume_ready: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Persistent project memory — all events across all sessions */
|
|
72
|
+
projectMemory: {
|
|
73
|
+
total_events: number;
|
|
74
|
+
session_count: number;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────
|
|
79
|
+
// AnalyticsEngine
|
|
80
|
+
// ─────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export class AnalyticsEngine {
|
|
83
|
+
private readonly db: DatabaseAdapter;
|
|
84
|
+
|
|
85
|
+
constructor(db: DatabaseAdapter) {
|
|
86
|
+
this.db = db;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a FullReport by merging runtime stats (passed in)
|
|
91
|
+
* with continuity data from the DB.
|
|
92
|
+
*/
|
|
93
|
+
queryAll(runtimeStats: RuntimeStats): FullReport {
|
|
94
|
+
// ── Resolve latest session ID ──
|
|
95
|
+
const latestSession = this.db.prepare(
|
|
96
|
+
"SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1",
|
|
97
|
+
).get() as { session_id: string } | undefined;
|
|
98
|
+
const sid = latestSession?.session_id ?? "";
|
|
99
|
+
|
|
100
|
+
// ── Runtime savings ──
|
|
101
|
+
const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce(
|
|
102
|
+
(sum, b) => sum + b, 0,
|
|
103
|
+
);
|
|
104
|
+
const totalCalls = Object.values(runtimeStats.calls).reduce(
|
|
105
|
+
(sum, c) => sum + c, 0,
|
|
106
|
+
);
|
|
107
|
+
const keptOut = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed;
|
|
108
|
+
const totalProcessed = keptOut + totalBytesReturned;
|
|
109
|
+
const savingsRatio = totalProcessed / Math.max(totalBytesReturned, 1);
|
|
110
|
+
const reductionPct = totalProcessed > 0
|
|
111
|
+
? Math.round((1 - totalBytesReturned / totalProcessed) * 100)
|
|
112
|
+
: 0;
|
|
113
|
+
|
|
114
|
+
const toolNames = new Set([
|
|
115
|
+
...Object.keys(runtimeStats.calls),
|
|
116
|
+
...Object.keys(runtimeStats.bytesReturned),
|
|
117
|
+
]);
|
|
118
|
+
const byTool = Array.from(toolNames).sort().map((tool) => ({
|
|
119
|
+
tool,
|
|
120
|
+
calls: runtimeStats.calls[tool] || 0,
|
|
121
|
+
context_kb: Math.round((runtimeStats.bytesReturned[tool] || 0) / 1024 * 10) / 10,
|
|
122
|
+
tokens: Math.round((runtimeStats.bytesReturned[tool] || 0) / 4),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const uptimeMs = Date.now() - runtimeStats.sessionStart;
|
|
126
|
+
const uptimeMin = (uptimeMs / 60_000).toFixed(1);
|
|
127
|
+
|
|
128
|
+
// ── Continuity data (scoped to current session) ──
|
|
129
|
+
const eventTotal = (this.db.prepare(
|
|
130
|
+
"SELECT COUNT(*) as cnt FROM session_events WHERE session_id = ?",
|
|
131
|
+
).get(sid) as { cnt: number }).cnt;
|
|
132
|
+
|
|
133
|
+
const meta = this.db.prepare(
|
|
134
|
+
"SELECT compact_count FROM session_meta WHERE session_id = ?",
|
|
135
|
+
).get(sid) as { compact_count: number } | undefined;
|
|
136
|
+
const compactCount = meta?.compact_count ?? 0;
|
|
137
|
+
|
|
138
|
+
const resume = this.db.prepare(
|
|
139
|
+
"SELECT event_count, consumed FROM session_resume WHERE session_id = ? ORDER BY created_at DESC LIMIT 1",
|
|
140
|
+
).get(sid) as { event_count: number; consumed: number } | undefined;
|
|
141
|
+
const resumeReady = resume ? !resume.consumed : false;
|
|
142
|
+
|
|
143
|
+
// ── Project-wide persistent memory (all sessions, no session_id filter) ──
|
|
144
|
+
const projectTotals = this.db.prepare(
|
|
145
|
+
"SELECT COUNT(*) as cnt, COUNT(DISTINCT session_id) as sessions FROM session_events",
|
|
146
|
+
).get() as { cnt: number; sessions: number };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
savings: {
|
|
150
|
+
processed_kb: Math.round(totalProcessed / 1024 * 10) / 10,
|
|
151
|
+
entered_kb: Math.round(totalBytesReturned / 1024 * 10) / 10,
|
|
152
|
+
saved_kb: Math.round(keptOut / 1024 * 10) / 10,
|
|
153
|
+
pct: reductionPct,
|
|
154
|
+
savings_ratio: Math.round(savingsRatio * 10) / 10,
|
|
155
|
+
by_tool: byTool,
|
|
156
|
+
total_calls: totalCalls,
|
|
157
|
+
total_bytes_returned: totalBytesReturned,
|
|
158
|
+
kept_out: keptOut,
|
|
159
|
+
total_processed: totalProcessed,
|
|
160
|
+
},
|
|
161
|
+
session: {
|
|
162
|
+
id: sid,
|
|
163
|
+
uptime_min: uptimeMin,
|
|
164
|
+
},
|
|
165
|
+
continuity: {
|
|
166
|
+
total_events: eventTotal,
|
|
167
|
+
compact_count: compactCount,
|
|
168
|
+
resume_ready: resumeReady,
|
|
169
|
+
},
|
|
170
|
+
projectMemory: {
|
|
171
|
+
total_events: projectTotals.cnt,
|
|
172
|
+
session_count: projectTotals.sessions,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─────────────────────────────────────────────────────────
|
|
179
|
+
// createMinimalDb — in-memory SQLite fallback
|
|
180
|
+
// ─────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create a minimal in-memory DatabaseAdapter for when SessionDB is unavailable.
|
|
184
|
+
* Returns zeroed/empty results for all queries.
|
|
185
|
+
*/
|
|
186
|
+
export function createMinimalDb(): DatabaseAdapter {
|
|
187
|
+
// Use an in-memory SQLite database with the expected schema
|
|
188
|
+
// so AnalyticsEngine queries don't fail.
|
|
189
|
+
const emptyStmt = {
|
|
190
|
+
run: (..._params: unknown[]) => {},
|
|
191
|
+
get: (..._params: unknown[]) => ({ cnt: 0, sessions: 0, compact_count: 0, session_id: "", event_count: 0, consumed: 1 }),
|
|
192
|
+
all: (..._params: unknown[]) => [] as unknown[],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
prepare: (_sql: string) => emptyStmt,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/session/db.ts
CHANGED
|
@@ -293,6 +293,9 @@ export class SessionDB {
|
|
|
293
293
|
return oldSessions.length;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/** Expose the underlying db for AnalyticsEngine (read-only queries). Returns null if init failed. */
|
|
297
|
+
getDb(): any { return this.db ?? null; }
|
|
298
|
+
|
|
296
299
|
close(): void {
|
|
297
300
|
try { this.db.close(); } catch { /* ignore */ }
|
|
298
301
|
}
|
package/src/tools/register.ts
CHANGED
|
@@ -137,8 +137,8 @@ function jsonResult(data: unknown, label?: string): any {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** Log a deprecation warning when old tool names are used. */
|
|
140
|
-
function deprecationLog(
|
|
141
|
-
|
|
140
|
+
function deprecationLog(_oldName: string, _newName: string): void {
|
|
141
|
+
// Deprecation logging disabled — was writing to stdout causing TUI rendering issues.
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// --- Old schema names for backward compat aliases ---
|
package/src/types.ts
CHANGED
|
@@ -283,11 +283,10 @@ export interface RuntimeCounters {
|
|
|
283
283
|
// ─────────────────────────────────────────────────────────
|
|
284
284
|
|
|
285
285
|
export interface CompactorInfoData {
|
|
286
|
-
sessionEvents: { value: string; detail: string };
|
|
287
|
-
compactions: { value: string; detail: string };
|
|
288
286
|
tokensSaved: { value: string; detail: string };
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
costSaved: { value: string; detail: string };
|
|
288
|
+
pctReduction: { value: string; detail: string };
|
|
289
|
+
topTools: { value: string; detail: string };
|
|
290
|
+
compactions: { value: string; detail: string };
|
|
291
|
+
toolCalls: { value: string; detail: string };
|
|
293
292
|
}
|