context-mode 1.0.103 → 1.0.105
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +39 -7
- package/bin/statusline.mjs +321 -0
- package/build/adapters/antigravity/index.d.ts +6 -0
- package/build/adapters/antigravity/index.js +10 -0
- package/build/adapters/base.d.ts +23 -0
- package/build/adapters/base.js +29 -0
- package/build/adapters/codex/index.d.ts +10 -0
- package/build/adapters/codex/index.js +22 -4
- package/build/adapters/cursor/index.d.ts +7 -0
- package/build/adapters/cursor/index.js +11 -0
- package/build/adapters/detect.d.ts +12 -1
- package/build/adapters/detect.js +69 -7
- package/build/adapters/gemini-cli/index.d.ts +8 -1
- package/build/adapters/gemini-cli/index.js +19 -7
- package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
- package/build/adapters/jetbrains-copilot/index.js +12 -0
- package/build/adapters/kiro/index.d.ts +8 -0
- package/build/adapters/kiro/index.js +12 -0
- package/build/adapters/openclaw/index.d.ts +17 -0
- package/build/adapters/openclaw/index.js +29 -4
- package/build/adapters/opencode/index.d.ts +8 -0
- package/build/adapters/opencode/index.js +18 -6
- package/build/adapters/qwen-code/index.d.ts +1 -0
- package/build/adapters/qwen-code/index.js +3 -0
- package/build/adapters/types.d.ts +33 -0
- package/build/adapters/vscode-copilot/index.d.ts +6 -0
- package/build/adapters/vscode-copilot/index.js +10 -0
- package/build/adapters/zed/index.d.ts +1 -0
- package/build/adapters/zed/index.js +3 -0
- package/build/cli.d.ts +15 -0
- package/build/cli.js +62 -16
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/executor.d.ts +11 -1
- package/build/executor.js +77 -21
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +6 -2
- package/build/lifecycle.js +29 -2
- package/build/opencode-plugin.d.ts +23 -0
- package/build/opencode-plugin.js +80 -6
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +54 -3
- package/build/search/auto-memory.d.ts +23 -10
- package/build/search/auto-memory.js +64 -26
- package/build/search/unified.d.ts +3 -0
- package/build/search/unified.js +2 -2
- package/build/server.d.ts +47 -0
- package/build/server.js +736 -188
- package/build/session/analytics.d.ts +49 -1
- package/build/session/analytics.js +278 -16
- package/build/session/db.d.ts +53 -8
- package/build/session/db.js +200 -19
- package/build/session/extract.js +124 -2
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/cli.bundle.mjs +208 -158
- package/configs/antigravity/GEMINI.md +11 -0
- package/configs/claude-code/CLAUDE.md +11 -0
- package/configs/codex/AGENTS.md +11 -0
- package/configs/cursor/context-mode.mdc +11 -0
- package/configs/gemini-cli/GEMINI.md +11 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
- package/configs/kilo/AGENTS.md +11 -0
- package/configs/kiro/KIRO.md +11 -0
- package/configs/openclaw/AGENTS.md +11 -0
- package/configs/opencode/AGENTS.md +11 -0
- package/configs/pi/AGENTS.md +11 -0
- package/configs/qwen-code/QWEN.md +11 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +11 -0
- package/hooks/auto-injection.mjs +36 -10
- package/hooks/cache-heal-utils.mjs +231 -0
- package/hooks/codex/sessionstart.mjs +7 -4
- package/hooks/core/routing.mjs +8 -2
- package/hooks/cursor/sessionstart.mjs +7 -4
- package/hooks/formatters/claude-code.mjs +20 -0
- package/hooks/gemini-cli/sessionstart.mjs +7 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
- package/hooks/normalize-hooks.mjs +184 -0
- package/hooks/session-db.bundle.mjs +41 -14
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +68 -20
- package/hooks/session-loaders.mjs +8 -2
- package/hooks/sessionstart.mjs +8 -2
- package/hooks/vscode-copilot/sessionstart.mjs +7 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +181 -134
- package/skills/ctx-doctor/SKILL.md +3 -3
- package/skills/ctx-insight/SKILL.md +1 -1
- package/start.mjs +63 -3
|
@@ -41,6 +41,13 @@ export interface SandboxIO {
|
|
|
41
41
|
inputBytes: number;
|
|
42
42
|
outputBytes: number;
|
|
43
43
|
}
|
|
44
|
+
/** MCP tool usage row — concurrency stats for batch-style tools. */
|
|
45
|
+
export interface McpToolUsageRow {
|
|
46
|
+
tool_name: string;
|
|
47
|
+
calls: number;
|
|
48
|
+
median_concurrency: number | null;
|
|
49
|
+
max_concurrency: number | null;
|
|
50
|
+
}
|
|
44
51
|
/** Runtime stats tracked by server.ts during a live session. */
|
|
45
52
|
export interface RuntimeStats {
|
|
46
53
|
bytesReturned: Record<string, number>;
|
|
@@ -150,6 +157,18 @@ export declare class AnalyticsEngine {
|
|
|
150
157
|
* Stub: requires PolyglotExecutor byte counters.
|
|
151
158
|
*/
|
|
152
159
|
static sandboxIO(inputBytes: number, outputBytes: number): SandboxIO;
|
|
160
|
+
/**
|
|
161
|
+
* MCP tool usage — call counts and concurrency stats per MCP tool.
|
|
162
|
+
*
|
|
163
|
+
* Reads `mcp_tool_call` events, parses the JSON payload, and aggregates:
|
|
164
|
+
* - call count per tool_name
|
|
165
|
+
* - median + max of `params.concurrency` (only for tools that take it,
|
|
166
|
+
* e.g. ctx_batch_execute, ctx_fetch_and_index). Returns null when the
|
|
167
|
+
* tool doesn't carry a concurrency param so callers can render N/A.
|
|
168
|
+
*
|
|
169
|
+
* Best-effort: malformed rows or truncated payloads are skipped silently.
|
|
170
|
+
*/
|
|
171
|
+
getMcpToolUsage(): McpToolUsageRow[];
|
|
153
172
|
/**
|
|
154
173
|
* Build a FullReport by merging runtime stats (passed in)
|
|
155
174
|
* with continuity data from the DB.
|
|
@@ -158,6 +177,32 @@ export declare class AnalyticsEngine {
|
|
|
158
177
|
*/
|
|
159
178
|
queryAll(runtimeStats: RuntimeStats): FullReport;
|
|
160
179
|
}
|
|
180
|
+
/** Aggregated stats spanning every SessionDB + auto-memory under the user's profile. */
|
|
181
|
+
export interface LifetimeStats {
|
|
182
|
+
totalEvents: number;
|
|
183
|
+
totalSessions: number;
|
|
184
|
+
autoMemoryCount: number;
|
|
185
|
+
autoMemoryProjects: number;
|
|
186
|
+
/** Per-prefix breakdown of auto-memory files (user/feedback/project/...). */
|
|
187
|
+
autoMemoryByPrefix: Record<string, number>;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Aggregate lifetime stats from all SessionDB files in `sessionsDir` and
|
|
191
|
+
* all auto-memory markdown files under `memoryRoot/<project>/memory/`.
|
|
192
|
+
*
|
|
193
|
+
* Best-effort: silently ignores missing/unreadable files so ctx_stats
|
|
194
|
+
* can never be broken by a corrupt sidecar.
|
|
195
|
+
*/
|
|
196
|
+
export declare function getLifetimeStats(opts?: {
|
|
197
|
+
sessionsDir?: string;
|
|
198
|
+
memoryRoot?: string;
|
|
199
|
+
/** Override for tests — defaults to db-base loadDatabase(). */
|
|
200
|
+
loadDatabase?: () => unknown;
|
|
201
|
+
}): LifetimeStats;
|
|
202
|
+
/** Opus 4 input price: $15 per 1M tokens. */
|
|
203
|
+
export declare const OPUS_INPUT_PRICE_PER_TOKEN: number;
|
|
204
|
+
/** Convert a token count to a USD string at the Opus input rate. */
|
|
205
|
+
export declare function tokensToUsd(tokens: number): string;
|
|
161
206
|
/**
|
|
162
207
|
* Render a FullReport as a visual savings dashboard designed for screenshotting.
|
|
163
208
|
*
|
|
@@ -168,4 +213,7 @@ export declare class AnalyticsEngine {
|
|
|
168
213
|
* - Project memory: category bars showing persistent data across sessions
|
|
169
214
|
* - No: Pct column, category tables, tips, jargon
|
|
170
215
|
*/
|
|
171
|
-
export declare function formatReport(report: FullReport, version?: string, latestVersion?: string | null
|
|
216
|
+
export declare function formatReport(report: FullReport, version?: string, latestVersion?: string | null, opts?: {
|
|
217
|
+
lifetime?: LifetimeStats;
|
|
218
|
+
mcpUsage?: McpToolUsageRow[];
|
|
219
|
+
}): string;
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* const engine = new AnalyticsEngine(sessionDb);
|
|
9
9
|
* const report = engine.queryAll(runtimeStats);
|
|
10
10
|
*/
|
|
11
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { loadDatabase as loadDatabaseImpl } from "../db-base.js";
|
|
11
15
|
function semverNewer(a, b) {
|
|
12
16
|
const pa = a.split(".").map(Number);
|
|
13
17
|
const pb = b.split(".").map(Number);
|
|
@@ -42,10 +46,10 @@ export const categoryLabels = {
|
|
|
42
46
|
};
|
|
43
47
|
/** Explains why each category matters for continuity. */
|
|
44
48
|
export const categoryHints = {
|
|
45
|
-
file: "Restored after compact
|
|
49
|
+
file: "Restored after compact — no need to re-read",
|
|
46
50
|
rule: "Your project instructions survive context resets",
|
|
47
51
|
prompt: "Continues exactly where you left off",
|
|
48
|
-
decision: "Applied automatically
|
|
52
|
+
decision: "Applied automatically — won’t ask again",
|
|
49
53
|
task: "Picks up from where it stopped",
|
|
50
54
|
error: "Tracked and monitored across compacts",
|
|
51
55
|
git: "Branch, commit, and repo state preserved",
|
|
@@ -115,6 +119,73 @@ export class AnalyticsEngine {
|
|
|
115
119
|
static sandboxIO(inputBytes, outputBytes) {
|
|
116
120
|
return { inputBytes, outputBytes };
|
|
117
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* MCP tool usage — call counts and concurrency stats per MCP tool.
|
|
124
|
+
*
|
|
125
|
+
* Reads `mcp_tool_call` events, parses the JSON payload, and aggregates:
|
|
126
|
+
* - call count per tool_name
|
|
127
|
+
* - median + max of `params.concurrency` (only for tools that take it,
|
|
128
|
+
* e.g. ctx_batch_execute, ctx_fetch_and_index). Returns null when the
|
|
129
|
+
* tool doesn't carry a concurrency param so callers can render N/A.
|
|
130
|
+
*
|
|
131
|
+
* Best-effort: malformed rows or truncated payloads are skipped silently.
|
|
132
|
+
*/
|
|
133
|
+
getMcpToolUsage() {
|
|
134
|
+
let rows;
|
|
135
|
+
try {
|
|
136
|
+
rows = this.db.prepare("SELECT data FROM session_events WHERE category = 'mcp_tool_call'").all();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
// toolName -> { calls, concurrencies }
|
|
142
|
+
const agg = new Map();
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(row.data);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : null;
|
|
152
|
+
if (!toolName)
|
|
153
|
+
continue;
|
|
154
|
+
const bucket = agg.get(toolName) ?? { calls: 0, concurrencies: [] };
|
|
155
|
+
bucket.calls += 1;
|
|
156
|
+
// Skip concurrency extraction when the row was truncated — the params
|
|
157
|
+
// blob is a substring of JSON that may not parse cleanly.
|
|
158
|
+
if (parsed.truncated !== true && parsed.params && typeof parsed.params === "object") {
|
|
159
|
+
const c = parsed.params.concurrency;
|
|
160
|
+
if (typeof c === "number" && Number.isFinite(c) && c > 0) {
|
|
161
|
+
bucket.concurrencies.push(c);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
agg.set(toolName, bucket);
|
|
165
|
+
}
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const [tool_name, b] of agg) {
|
|
168
|
+
let median = null;
|
|
169
|
+
let max = null;
|
|
170
|
+
if (b.concurrencies.length > 0) {
|
|
171
|
+
const sorted = [...b.concurrencies].sort((a, c) => a - c);
|
|
172
|
+
const mid = Math.floor(sorted.length / 2);
|
|
173
|
+
median = sorted.length % 2 === 0
|
|
174
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
175
|
+
: sorted[mid];
|
|
176
|
+
max = sorted[sorted.length - 1];
|
|
177
|
+
}
|
|
178
|
+
out.push({
|
|
179
|
+
tool_name,
|
|
180
|
+
calls: b.calls,
|
|
181
|
+
median_concurrency: median,
|
|
182
|
+
max_concurrency: max,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Stable sort: most-called first, then alphabetical
|
|
186
|
+
out.sort((a, c) => c.calls - a.calls || a.tool_name.localeCompare(c.tool_name));
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
118
189
|
// ═══════════════════════════════════════════════════════
|
|
119
190
|
// queryAll — single unified report from ONE source
|
|
120
191
|
// ═══════════════════════════════════════════════════════
|
|
@@ -241,6 +312,110 @@ export class AnalyticsEngine {
|
|
|
241
312
|
};
|
|
242
313
|
}
|
|
243
314
|
}
|
|
315
|
+
/** Extract leading prefix from auto-memory filename: `feedback_push.md` → `feedback`. */
|
|
316
|
+
function autoMemoryPrefix(filename) {
|
|
317
|
+
const base = filename.replace(/\.md$/i, "");
|
|
318
|
+
const m = base.match(/^([a-z]+)/i);
|
|
319
|
+
return m ? m[1].toLowerCase() : "other";
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Aggregate lifetime stats from all SessionDB files in `sessionsDir` and
|
|
323
|
+
* all auto-memory markdown files under `memoryRoot/<project>/memory/`.
|
|
324
|
+
*
|
|
325
|
+
* Best-effort: silently ignores missing/unreadable files so ctx_stats
|
|
326
|
+
* can never be broken by a corrupt sidecar.
|
|
327
|
+
*/
|
|
328
|
+
export function getLifetimeStats(opts) {
|
|
329
|
+
const sessionsDir = opts?.sessionsDir
|
|
330
|
+
?? join(homedir(), ".claude", "context-mode", "sessions");
|
|
331
|
+
const memoryRoot = opts?.memoryRoot
|
|
332
|
+
?? join(homedir(), ".claude", "projects");
|
|
333
|
+
let totalEvents = 0;
|
|
334
|
+
let totalSessions = 0;
|
|
335
|
+
// ── SessionDB aggregation ──
|
|
336
|
+
if (existsSync(sessionsDir)) {
|
|
337
|
+
let dbFiles = [];
|
|
338
|
+
try {
|
|
339
|
+
dbFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".db"));
|
|
340
|
+
}
|
|
341
|
+
catch { /* unreadable */ }
|
|
342
|
+
if (dbFiles.length > 0) {
|
|
343
|
+
// Lazy-load better-sqlite3 / bun-sqlite via the same path the runtime uses.
|
|
344
|
+
let DatabaseCtor = null;
|
|
345
|
+
try {
|
|
346
|
+
DatabaseCtor = opts?.loadDatabase
|
|
347
|
+
? opts.loadDatabase()
|
|
348
|
+
: loadDatabaseImpl();
|
|
349
|
+
}
|
|
350
|
+
catch { /* sqlite unavailable */ }
|
|
351
|
+
if (DatabaseCtor) {
|
|
352
|
+
for (const file of dbFiles) {
|
|
353
|
+
const dbPath = join(sessionsDir, file);
|
|
354
|
+
try {
|
|
355
|
+
const sdb = new DatabaseCtor(dbPath, { readonly: true });
|
|
356
|
+
try {
|
|
357
|
+
const ev = sdb.prepare("SELECT COUNT(*) AS cnt FROM session_events").get();
|
|
358
|
+
const ss = sdb.prepare("SELECT COUNT(*) AS cnt FROM session_meta").get();
|
|
359
|
+
totalEvents += ev?.cnt ?? 0;
|
|
360
|
+
totalSessions += ss?.cnt ?? 0;
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
sdb.close();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// missing tables / corrupt file — skip
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ── Auto-memory file scan ──
|
|
374
|
+
let autoMemoryCount = 0;
|
|
375
|
+
let autoMemoryProjects = 0;
|
|
376
|
+
const autoMemoryByPrefix = {};
|
|
377
|
+
if (existsSync(memoryRoot)) {
|
|
378
|
+
let projectDirs = [];
|
|
379
|
+
try {
|
|
380
|
+
projectDirs = readdirSync(memoryRoot).filter((entry) => {
|
|
381
|
+
try {
|
|
382
|
+
return statSync(join(memoryRoot, entry)).isDirectory();
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch { /* unreadable */ }
|
|
390
|
+
for (const proj of projectDirs) {
|
|
391
|
+
const memDir = join(memoryRoot, proj, "memory");
|
|
392
|
+
if (!existsSync(memDir))
|
|
393
|
+
continue;
|
|
394
|
+
let mdFiles = [];
|
|
395
|
+
try {
|
|
396
|
+
mdFiles = readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (mdFiles.length === 0)
|
|
402
|
+
continue;
|
|
403
|
+
autoMemoryProjects++;
|
|
404
|
+
autoMemoryCount += mdFiles.length;
|
|
405
|
+
for (const f of mdFiles) {
|
|
406
|
+
const prefix = autoMemoryPrefix(f);
|
|
407
|
+
autoMemoryByPrefix[prefix] = (autoMemoryByPrefix[prefix] ?? 0) + 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
totalEvents,
|
|
413
|
+
totalSessions,
|
|
414
|
+
autoMemoryCount,
|
|
415
|
+
autoMemoryProjects,
|
|
416
|
+
autoMemoryByPrefix,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
244
419
|
// ─────────────────────────────────────────────────────────
|
|
245
420
|
// formatReport — renders FullReport as sales-grade savings dashboard
|
|
246
421
|
// ─────────────────────────────────────────────────────────
|
|
@@ -271,6 +446,16 @@ function fmtNum(n) {
|
|
|
271
446
|
return `${(n / 1_000).toFixed(1)}K`;
|
|
272
447
|
return String(n);
|
|
273
448
|
}
|
|
449
|
+
// ─────────────────────────────────────────────────────────
|
|
450
|
+
// Pricing (Bug #6) — Anthropic Opus input rate
|
|
451
|
+
// ─────────────────────────────────────────────────────────
|
|
452
|
+
/** Opus 4 input price: $15 per 1M tokens. */
|
|
453
|
+
export const OPUS_INPUT_PRICE_PER_TOKEN = 15 / 1_000_000;
|
|
454
|
+
/** Convert a token count to a USD string at the Opus input rate. */
|
|
455
|
+
export function tokensToUsd(tokens) {
|
|
456
|
+
const safe = Number.isFinite(tokens) && tokens > 0 ? tokens : 0;
|
|
457
|
+
return `$${(safe * OPUS_INPUT_PRICE_PER_TOKEN).toFixed(2)}`;
|
|
458
|
+
}
|
|
274
459
|
/**
|
|
275
460
|
* Build a proportional bar using █ chars, scaled to a fixed width.
|
|
276
461
|
* Returns e.g. "████████████████████████████████████████" for full width.
|
|
@@ -283,20 +468,72 @@ function dataBar(bytes, maxBytes, width = 40) {
|
|
|
283
468
|
}
|
|
284
469
|
/**
|
|
285
470
|
* Render project memory section with category bars.
|
|
286
|
-
*
|
|
471
|
+
*
|
|
472
|
+
* Shows persistent event data, and — when supplied — lifetime totals
|
|
473
|
+
* across every project's SessionDB so users see the cumulative value
|
|
474
|
+
* (Bug #3).
|
|
475
|
+
*
|
|
476
|
+
* Caps the category list at `topN` and prints "N more categories" with the
|
|
477
|
+
* actual remaining count (Bug #5 — was hardcoded "9 more").
|
|
287
478
|
*/
|
|
288
|
-
function renderProjectMemory(pm) {
|
|
289
|
-
if (pm.total_events === 0)
|
|
479
|
+
function renderProjectMemory(pm, opts) {
|
|
480
|
+
if (pm.total_events === 0 && (opts?.lifetime?.totalEvents ?? 0) === 0)
|
|
290
481
|
return [];
|
|
482
|
+
const topN = opts?.topN ?? 2;
|
|
291
483
|
const out = [];
|
|
292
484
|
out.push("");
|
|
293
|
-
|
|
294
|
-
|
|
485
|
+
out.push("Persistent memory ✓ preserved across compact, restart & upgrade");
|
|
486
|
+
// Lifetime line (Bug #3) — collapses to project-only when lifetime missing.
|
|
487
|
+
const lifeEvents = opts?.lifetime?.totalEvents ?? pm.total_events;
|
|
488
|
+
const lifeSessions = opts?.lifetime?.totalSessions ?? pm.session_count;
|
|
489
|
+
const sessionLabel = lifeSessions === 1 ? "1 session" : `${fmtNum(lifeSessions)} sessions`;
|
|
490
|
+
// Estimate lifetime savings: ~1KB per event → ~256 tokens/event at Opus rates.
|
|
491
|
+
const lifetimeTokens = lifeEvents * 256;
|
|
492
|
+
out.push(` ${fmtNum(lifeEvents)} events · ${sessionLabel} · ~${tokensToUsd(lifetimeTokens)} saved lifetime`);
|
|
295
493
|
out.push("");
|
|
296
|
-
const
|
|
297
|
-
|
|
494
|
+
const cats = pm.by_category;
|
|
495
|
+
const visible = cats.slice(0, topN);
|
|
496
|
+
const maxCount = visible.length > 0 ? visible[0].count : 1;
|
|
497
|
+
for (const cat of visible) {
|
|
298
498
|
out.push(` ${cat.label.padEnd(18)} ${String(cat.count).padStart(5)} ${dataBar(cat.count, maxCount, 30)}`);
|
|
299
499
|
}
|
|
500
|
+
// Bug #5: real overflow count, not hardcoded.
|
|
501
|
+
const remaining = Math.max(0, cats.length - topN);
|
|
502
|
+
if (remaining > 0) {
|
|
503
|
+
out.push(` ... ${remaining} more categor${remaining === 1 ? "y" : "ies"}`);
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Render the auto-memory section (Bug #4) — files Claude Code captured
|
|
509
|
+
* under ~/.claude/projects/<project>/memory/ across the user's machine.
|
|
510
|
+
*/
|
|
511
|
+
function renderAutoMemory(lifetime) {
|
|
512
|
+
if (!lifetime || lifetime.autoMemoryCount === 0)
|
|
513
|
+
return [];
|
|
514
|
+
const out = [];
|
|
515
|
+
out.push("");
|
|
516
|
+
out.push(`Auto-memory ✓ ${lifetime.autoMemoryCount} preference${lifetime.autoMemoryCount === 1 ? "" : "s"} learned across ${lifetime.autoMemoryProjects} project${lifetime.autoMemoryProjects === 1 ? "" : "s"}`);
|
|
517
|
+
const entries = Object.entries(lifetime.autoMemoryByPrefix)
|
|
518
|
+
.sort((a, b) => b[1] - a[1])
|
|
519
|
+
.slice(0, 6);
|
|
520
|
+
for (const [prefix, count] of entries) {
|
|
521
|
+
out.push(` ${prefix.padEnd(12)} ${String(count).padStart(2)}`);
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
/** Render the closing "Bottom line" footer (Bug #8). */
|
|
526
|
+
function renderBottomLine(sessionTokensSaved, lifetime) {
|
|
527
|
+
const out = [];
|
|
528
|
+
const sessionUsd = tokensToUsd(sessionTokensSaved);
|
|
529
|
+
// Lifetime estimate: ~1KB/event ÷ 4 bytes/token = 256 tokens/event.
|
|
530
|
+
const lifetimeTokens = (lifetime?.totalEvents ?? 0) * 256;
|
|
531
|
+
const lifetimeUsd = tokensToUsd(lifetimeTokens);
|
|
532
|
+
out.push("");
|
|
533
|
+
out.push("─".repeat(65));
|
|
534
|
+
out.push("Your AI talks less, remembers more, costs less.");
|
|
535
|
+
out.push(`${sessionUsd} this session · ${lifetimeUsd} lifetime`);
|
|
536
|
+
out.push("─".repeat(65));
|
|
300
537
|
return out;
|
|
301
538
|
}
|
|
302
539
|
/**
|
|
@@ -309,9 +546,11 @@ function renderProjectMemory(pm) {
|
|
|
309
546
|
* - Project memory: category bars showing persistent data across sessions
|
|
310
547
|
* - No: Pct column, category tables, tips, jargon
|
|
311
548
|
*/
|
|
312
|
-
export function formatReport(report, version, latestVersion) {
|
|
549
|
+
export function formatReport(report, version, latestVersion, opts) {
|
|
313
550
|
const lines = [];
|
|
314
551
|
const duration = formatDuration(report.session.uptime_min);
|
|
552
|
+
const lifetime = opts?.lifetime;
|
|
553
|
+
const mcpUsage = opts?.mcpUsage;
|
|
315
554
|
// ── Compute real savings ──
|
|
316
555
|
const totalKeptOut = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
|
|
317
556
|
const totalReturned = report.savings.total_bytes_returned;
|
|
@@ -319,6 +558,9 @@ export function formatReport(report, version, latestVersion) {
|
|
|
319
558
|
const grandTotal = totalKeptOut + totalReturned;
|
|
320
559
|
const savingsPct = grandTotal > 0 ? (totalKeptOut / grandTotal) * 100 : 0;
|
|
321
560
|
const tokensSaved = Math.round(totalKeptOut / 4);
|
|
561
|
+
const ratioMultiplier = totalReturned > 0
|
|
562
|
+
? Math.max(1, Math.round(grandTotal / Math.max(totalReturned, 1)))
|
|
563
|
+
: 0;
|
|
322
564
|
// ── Fresh session: no savings yet ──
|
|
323
565
|
if (totalKeptOut === 0) {
|
|
324
566
|
lines.push(`context-mode ${duration} ${totalCalls} calls`);
|
|
@@ -329,8 +571,10 @@ export function formatReport(report, version, latestVersion) {
|
|
|
329
571
|
else {
|
|
330
572
|
lines.push(`${kb(totalReturned)} entered context | 0 tokens saved`);
|
|
331
573
|
}
|
|
332
|
-
// Project memory
|
|
333
|
-
lines.push(...renderProjectMemory(report.projectMemory));
|
|
574
|
+
// Project memory + auto-memory + bottom line
|
|
575
|
+
lines.push(...renderProjectMemory(report.projectMemory, { lifetime }));
|
|
576
|
+
lines.push(...renderAutoMemory(lifetime));
|
|
577
|
+
lines.push(...renderBottomLine(0, lifetime));
|
|
334
578
|
// Footer
|
|
335
579
|
lines.push("");
|
|
336
580
|
const versionStr = version ? `v${version}` : "context-mode";
|
|
@@ -342,14 +586,21 @@ export function formatReport(report, version, latestVersion) {
|
|
|
342
586
|
}
|
|
343
587
|
// ── Active session: visual savings dashboard ──
|
|
344
588
|
// Line 1: Hero metric — the screenshottable number
|
|
345
|
-
|
|
589
|
+
// Bug #6: include Opus pricing on the hero line for credibility.
|
|
590
|
+
lines.push(`${fmtNum(tokensSaved)} tokens saved · ${savingsPct.toFixed(1)}% reduction · ${duration} · ~${tokensToUsd(tokensSaved)} saved (Opus)`);
|
|
346
591
|
lines.push("");
|
|
347
592
|
// Lines 2-3: Before/After comparison bars — the visual proof
|
|
348
593
|
lines.push(`Without context-mode |${dataBar(grandTotal, grandTotal)}| ${kb(grandTotal)}`);
|
|
349
594
|
lines.push(`With context-mode |${dataBar(totalReturned, grandTotal)}| ${kb(totalReturned)}`);
|
|
350
595
|
lines.push("");
|
|
351
596
|
// Value statement — the line people share
|
|
352
|
-
|
|
597
|
+
// Bug #7: replace meaningless "3.0x" ratio with "3× longer sessions".
|
|
598
|
+
if (ratioMultiplier >= 2) {
|
|
599
|
+
lines.push(`${kb(totalKeptOut)} kept out of your conversation — ${ratioMultiplier}× longer sessions before compact.`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
lines.push(`${kb(totalKeptOut)} kept out of your conversation. Never entered context.`);
|
|
603
|
+
}
|
|
353
604
|
lines.push("");
|
|
354
605
|
// Compact stats row
|
|
355
606
|
const statParts = [`${totalCalls} calls`];
|
|
@@ -376,8 +627,19 @@ export function formatReport(report, version, latestVersion) {
|
|
|
376
627
|
lines.push(` ${name.padEnd(22)} ${String(t.calls).padStart(4)} calls ${kb(t.estimatedSaved).padStart(8)} saved`);
|
|
377
628
|
}
|
|
378
629
|
}
|
|
379
|
-
// ──
|
|
380
|
-
|
|
630
|
+
// ── MCP concurrency usage (only when batch tools recorded a concurrency) ──
|
|
631
|
+
if (mcpUsage && mcpUsage.length > 0) {
|
|
632
|
+
const concurrent = mcpUsage.filter((u) => u.median_concurrency != null);
|
|
633
|
+
for (const u of concurrent) {
|
|
634
|
+
lines.push(`MCP concurrency usage: ${u.tool_name} median=${u.median_concurrency} max=${u.max_concurrency} (${u.calls} calls)`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// ── Project memory — persistent across sessions (Bug #3 + #5) ──
|
|
638
|
+
lines.push(...renderProjectMemory(report.projectMemory, { lifetime }));
|
|
639
|
+
// ── Auto-memory — Claude Code's preference learnings (Bug #4) ──
|
|
640
|
+
lines.push(...renderAutoMemory(lifetime));
|
|
641
|
+
// ── Bottom line — business value framing (Bug #8) ──
|
|
642
|
+
lines.push(...renderBottomLine(tokensSaved, lifetime));
|
|
381
643
|
// ── Footer ──
|
|
382
644
|
lines.push("");
|
|
383
645
|
const versionStr = version ? `v${version}` : "context-mode";
|
package/build/session/db.d.ts
CHANGED
|
@@ -8,15 +8,8 @@
|
|
|
8
8
|
import { SQLiteBase } from "../db-base.js";
|
|
9
9
|
import type { SessionEvent } from "../types.js";
|
|
10
10
|
import type { ProjectAttribution } from "./project-attribution.js";
|
|
11
|
-
/**
|
|
12
|
-
* Returns the worktree suffix to append to session identifiers.
|
|
13
|
-
* Returns empty string when running in the main working tree.
|
|
14
|
-
*
|
|
15
|
-
* Set CONTEXT_MODE_SESSION_SUFFIX to an explicit value to override
|
|
16
|
-
* (useful in CI environments or when git is unavailable).
|
|
17
|
-
* Set to empty string to disable isolation entirely.
|
|
18
|
-
*/
|
|
19
11
|
export declare function getWorktreeSuffix(): string;
|
|
12
|
+
export declare function _resetWorktreeSuffixCacheForTests(): void;
|
|
20
13
|
/** A stored event row from the session_events table. */
|
|
21
14
|
export interface StoredEvent {
|
|
22
15
|
id: number;
|
|
@@ -47,6 +40,15 @@ export interface ResumeRow {
|
|
|
47
40
|
event_count: number;
|
|
48
41
|
consumed: number;
|
|
49
42
|
}
|
|
43
|
+
/** Aggregated tool-call stats for a single session. */
|
|
44
|
+
export interface ToolCallStats {
|
|
45
|
+
totalCalls: number;
|
|
46
|
+
totalBytesReturned: number;
|
|
47
|
+
byTool: Record<string, {
|
|
48
|
+
calls: number;
|
|
49
|
+
bytesReturned: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
50
52
|
export declare class SessionDB extends SQLiteBase {
|
|
51
53
|
/**
|
|
52
54
|
* Cached prepared statements. Stored in a Map to avoid the JS private-field
|
|
@@ -76,6 +78,18 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
76
78
|
* lowest-priority (then oldest) event.
|
|
77
79
|
*/
|
|
78
80
|
insertEvent(sessionId: string, event: SessionEvent, sourceHook?: string, attribution?: Partial<ProjectAttribution>): void;
|
|
81
|
+
/**
|
|
82
|
+
* Bulk-insert N events in a SINGLE transaction.
|
|
83
|
+
*
|
|
84
|
+
* PostToolUse hooks emit 5–15 events per tool call. Calling insertEvent()
|
|
85
|
+
* in a loop runs N transactions = N WAL commits = N fsync candidates,
|
|
86
|
+
* which is painful on Windows NTFS where commit latency dominates.
|
|
87
|
+
* One transaction = one commit, dedup/evict checks reuse cached statements.
|
|
88
|
+
*
|
|
89
|
+
* Cross-platform: uses the same WAL-mode transaction primitive as
|
|
90
|
+
* insertEvent — behavior identical on macOS / Linux / Windows.
|
|
91
|
+
*/
|
|
92
|
+
bulkInsertEvents(sessionId: string, events: SessionEvent[], sourceHook?: string, attributions?: Array<Partial<ProjectAttribution> | undefined>): void;
|
|
79
93
|
/**
|
|
80
94
|
* Retrieve events for a session with optional filtering.
|
|
81
95
|
*/
|
|
@@ -134,6 +148,37 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
134
148
|
* Mark the resume snapshot as consumed (already injected into conversation).
|
|
135
149
|
*/
|
|
136
150
|
markResumeConsumed(sessionId: string): void;
|
|
151
|
+
/**
|
|
152
|
+
* Atomically claim the most recent unconsumed resume snapshot in this DB.
|
|
153
|
+
*
|
|
154
|
+
* `SessionDB` is sharded per project (see `getSessionDBPath` — SHA-256 of
|
|
155
|
+
* project dir), so "this DB" already implies "this project". The atomic
|
|
156
|
+
* `UPDATE … RETURNING` ensures concurrent processes for the same project
|
|
157
|
+
* cannot both inject the same snapshot (Mickey / PR #376 race).
|
|
158
|
+
*
|
|
159
|
+
* Returns null when no unconsumed snapshot exists.
|
|
160
|
+
*/
|
|
161
|
+
claimLatestUnconsumedResume(): {
|
|
162
|
+
sessionId: string;
|
|
163
|
+
snapshot: string;
|
|
164
|
+
} | null;
|
|
165
|
+
/**
|
|
166
|
+
* Return the most recent session_id from session_meta, or null if none.
|
|
167
|
+
* Used by the runtime to attach persistent counters to the right session
|
|
168
|
+
* after a process restart.
|
|
169
|
+
*/
|
|
170
|
+
getLatestSessionId(): string | null;
|
|
171
|
+
/**
|
|
172
|
+
* Increment the persistent tool-call counter for `tool` in `sessionId`.
|
|
173
|
+
* Adds `bytesReturned` to the cumulative total. Idempotent across
|
|
174
|
+
* SessionDB instances — counters survive process restart.
|
|
175
|
+
*/
|
|
176
|
+
incrementToolCall(sessionId: string, tool: string, bytesReturned?: number): void;
|
|
177
|
+
/**
|
|
178
|
+
* Get aggregated tool-call stats for `sessionId`. Returns zero-stats
|
|
179
|
+
* when the session has no recorded calls.
|
|
180
|
+
*/
|
|
181
|
+
getToolCallStats(sessionId: string): ToolCallStats;
|
|
137
182
|
/**
|
|
138
183
|
* Delete all data for a session (events, meta, resume).
|
|
139
184
|
*/
|