context-mode 1.0.71 → 1.0.73
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/build/server.js +33 -7
- package/build/session/analytics.d.ts +13 -233
- package/build/session/analytics.js +118 -507
- package/cli.bundle.mjs +145 -204
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +88 -147
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AnalyticsEngine —
|
|
2
|
+
* AnalyticsEngine — Runtime savings + session continuity reporting.
|
|
3
3
|
*
|
|
4
|
-
* Computes
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Group 1 (SQL Direct): 17 metrics — direct SQL against session tables
|
|
8
|
-
* Group 2 (JS Computed): 3 metrics — SQL + JS post-processing
|
|
9
|
-
* Group 3 (Runtime): 4 metrics — stubs for server.ts tracking
|
|
10
|
-
* Group 4 (New Extractor): 3 metrics — stubs for future extractors
|
|
4
|
+
* Computes context-window savings from runtime stats and queries
|
|
5
|
+
* session continuity data from SessionDB.
|
|
11
6
|
*
|
|
12
7
|
* Usage:
|
|
13
8
|
* const engine = new AnalyticsEngine(sessionDb);
|
|
@@ -64,273 +59,6 @@ export class AnalyticsEngine {
|
|
|
64
59
|
this.db = db;
|
|
65
60
|
}
|
|
66
61
|
// ═══════════════════════════════════════════════════════
|
|
67
|
-
// GROUP 1 — SQL Direct (17 metrics)
|
|
68
|
-
// ═══════════════════════════════════════════════════════
|
|
69
|
-
/**
|
|
70
|
-
* #5 Weekly Trend — sessions started per day over the last 7 days.
|
|
71
|
-
* Returns an array of { day, sessions } sorted by day.
|
|
72
|
-
*/
|
|
73
|
-
weeklyTrend() {
|
|
74
|
-
return this.db.prepare(`SELECT date(started_at) as day, COUNT(*) as sessions
|
|
75
|
-
FROM session_meta
|
|
76
|
-
WHERE started_at > datetime('now', '-7 days')
|
|
77
|
-
GROUP BY day`).all();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* #7 Session Continuity — event category distribution for a session.
|
|
81
|
-
* Shows what the session focused on (file ops, git, errors, etc.).
|
|
82
|
-
*/
|
|
83
|
-
sessionContinuity(sessionId) {
|
|
84
|
-
return this.db.prepare(`SELECT category, COUNT(*) as count
|
|
85
|
-
FROM session_events
|
|
86
|
-
WHERE session_id = ?
|
|
87
|
-
GROUP BY category`).all(sessionId);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* #8 Commit Count — number of git commits made during a session.
|
|
91
|
-
* Matches events where category='git' and data contains 'commit'.
|
|
92
|
-
*/
|
|
93
|
-
commitCount(sessionId) {
|
|
94
|
-
const row = this.db.prepare(`SELECT COUNT(*) as cnt
|
|
95
|
-
FROM session_events
|
|
96
|
-
WHERE session_id = ? AND category = 'git' AND data LIKE '%commit%'`).get(sessionId);
|
|
97
|
-
return row.cnt;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* #9 Error Count — total error events in a session.
|
|
101
|
-
*/
|
|
102
|
-
errorCount(sessionId) {
|
|
103
|
-
const row = this.db.prepare(`SELECT COUNT(*) as cnt
|
|
104
|
-
FROM session_events
|
|
105
|
-
WHERE session_id = ? AND category = 'error'`).get(sessionId);
|
|
106
|
-
return row.cnt;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* #10 Session Duration — elapsed minutes from session start to last event.
|
|
110
|
-
* Returns null if last_event_at is not set (session still initializing).
|
|
111
|
-
*/
|
|
112
|
-
sessionDuration(sessionId) {
|
|
113
|
-
const row = this.db.prepare(`SELECT (julianday(last_event_at) - julianday(started_at)) * 24 * 60 as minutes
|
|
114
|
-
FROM session_meta
|
|
115
|
-
WHERE session_id = ?`).get(sessionId);
|
|
116
|
-
return row?.minutes ?? null;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* #11 Error Rate — percentage of events that are errors in a session.
|
|
120
|
-
* Returns 0 for sessions with no events (division by zero protection).
|
|
121
|
-
*/
|
|
122
|
-
errorRate(sessionId) {
|
|
123
|
-
const row = this.db.prepare(`SELECT ROUND(100.0 * SUM(CASE WHEN category='error' THEN 1 ELSE 0 END) / COUNT(*), 1) as rate
|
|
124
|
-
FROM session_events
|
|
125
|
-
WHERE session_id = ?`).get(sessionId);
|
|
126
|
-
return row.rate ?? 0;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* #12 Tool Diversity — number of distinct MCP tools used in a session.
|
|
130
|
-
* Higher diversity suggests more sophisticated tool usage.
|
|
131
|
-
*/
|
|
132
|
-
toolDiversity(sessionId) {
|
|
133
|
-
const row = this.db.prepare(`SELECT COUNT(DISTINCT data) as cnt
|
|
134
|
-
FROM session_events
|
|
135
|
-
WHERE session_id = ? AND category = 'mcp'`).get(sessionId);
|
|
136
|
-
return row.cnt;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* #14 Hourly Productivity — event distribution by hour of day.
|
|
140
|
-
* Optionally scoped to a session; omit sessionId for all sessions.
|
|
141
|
-
*/
|
|
142
|
-
hourlyProductivity(sessionId) {
|
|
143
|
-
if (sessionId) {
|
|
144
|
-
return this.db.prepare(`SELECT strftime('%H', created_at) as hour, COUNT(*) as count
|
|
145
|
-
FROM session_events
|
|
146
|
-
WHERE session_id = ?
|
|
147
|
-
GROUP BY hour`).all(sessionId);
|
|
148
|
-
}
|
|
149
|
-
return this.db.prepare(`SELECT strftime('%H', created_at) as hour, COUNT(*) as count
|
|
150
|
-
FROM session_events
|
|
151
|
-
GROUP BY hour`).all();
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* #15 Project Distribution — session count per project directory.
|
|
155
|
-
* Sorted descending by session count.
|
|
156
|
-
*/
|
|
157
|
-
projectDistribution() {
|
|
158
|
-
return this.db.prepare(`SELECT project_dir, COUNT(*) as sessions
|
|
159
|
-
FROM session_meta
|
|
160
|
-
GROUP BY project_dir
|
|
161
|
-
ORDER BY sessions DESC`).all();
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* #16 Compaction Count — number of snapshot compactions for a session.
|
|
165
|
-
* Higher counts indicate longer/more active sessions.
|
|
166
|
-
*/
|
|
167
|
-
compactionCount(sessionId) {
|
|
168
|
-
const row = this.db.prepare(`SELECT compact_count
|
|
169
|
-
FROM session_meta
|
|
170
|
-
WHERE session_id = ?`).get(sessionId);
|
|
171
|
-
return row?.compact_count ?? 0;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* #17 Weekly Session Count — total sessions started in the last 7 days.
|
|
175
|
-
*/
|
|
176
|
-
weeklySessionCount() {
|
|
177
|
-
const row = this.db.prepare(`SELECT COUNT(*) as cnt
|
|
178
|
-
FROM session_meta
|
|
179
|
-
WHERE started_at > datetime('now', '-7 days')`).get();
|
|
180
|
-
return row.cnt;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* #18 Commits Per Session — average commits across all sessions.
|
|
184
|
-
* Returns 0 when no sessions exist (NULLIF prevents division by zero).
|
|
185
|
-
*/
|
|
186
|
-
commitsPerSession() {
|
|
187
|
-
const row = this.db.prepare(`SELECT ROUND(1.0 * (SELECT COUNT(*) FROM session_events WHERE category='git' AND data LIKE '%commit%')
|
|
188
|
-
/ NULLIF((SELECT COUNT(DISTINCT session_id) FROM session_meta), 0), 1) as avg`).get();
|
|
189
|
-
return row.avg ?? 0;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* #22 CLAUDE.md Freshness — last update timestamp for each rule file.
|
|
193
|
-
* Helps identify stale configuration files.
|
|
194
|
-
*/
|
|
195
|
-
claudeMdFreshness() {
|
|
196
|
-
return this.db.prepare(`SELECT data, MAX(created_at) as last_updated
|
|
197
|
-
FROM session_events
|
|
198
|
-
WHERE category = 'rule'
|
|
199
|
-
GROUP BY data`).all();
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* #24 Rework Rate — files edited more than once (indicates iteration/rework).
|
|
203
|
-
* Sorted descending by edit count.
|
|
204
|
-
*/
|
|
205
|
-
reworkRate(sessionId) {
|
|
206
|
-
if (sessionId) {
|
|
207
|
-
return this.db.prepare(`SELECT data, COUNT(*) as edits
|
|
208
|
-
FROM session_events
|
|
209
|
-
WHERE session_id = ? AND category = 'file'
|
|
210
|
-
GROUP BY data
|
|
211
|
-
HAVING edits > 1
|
|
212
|
-
ORDER BY edits DESC`).all(sessionId);
|
|
213
|
-
}
|
|
214
|
-
return this.db.prepare(`SELECT data, COUNT(*) as edits
|
|
215
|
-
FROM session_events
|
|
216
|
-
WHERE category = 'file'
|
|
217
|
-
GROUP BY data
|
|
218
|
-
HAVING edits > 1
|
|
219
|
-
ORDER BY edits DESC`).all();
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* #25 Session Outcome — classify a session as 'productive' or 'exploratory'.
|
|
223
|
-
* Productive: has at least one commit AND last event is not an error.
|
|
224
|
-
*/
|
|
225
|
-
sessionOutcome(sessionId) {
|
|
226
|
-
const row = this.db.prepare(`
|
|
227
|
-
SELECT CASE
|
|
228
|
-
WHEN EXISTS(SELECT 1 FROM session_events WHERE session_id=? AND category='git' AND data LIKE '%commit%')
|
|
229
|
-
AND NOT EXISTS(SELECT 1 FROM session_events WHERE session_id=?
|
|
230
|
-
AND category='error' AND id=(SELECT MAX(id) FROM session_events WHERE session_id=?))
|
|
231
|
-
THEN 'productive'
|
|
232
|
-
ELSE 'exploratory'
|
|
233
|
-
END as outcome
|
|
234
|
-
`).get(sessionId, sessionId, sessionId);
|
|
235
|
-
return row.outcome;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* #26 Subagent Usage — subagent spawn counts grouped by type/purpose.
|
|
239
|
-
*/
|
|
240
|
-
subagentUsage(sessionId) {
|
|
241
|
-
return this.db.prepare(`SELECT COUNT(*) as total, data
|
|
242
|
-
FROM session_events
|
|
243
|
-
WHERE session_id = ? AND category = 'subagent'
|
|
244
|
-
GROUP BY data`).all(sessionId);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* #27 Skill Usage — skill/slash-command invocation frequency.
|
|
248
|
-
* Sorted descending by invocation count.
|
|
249
|
-
*/
|
|
250
|
-
skillUsage(sessionId) {
|
|
251
|
-
return this.db.prepare(`SELECT data, COUNT(*) as invocations
|
|
252
|
-
FROM session_events
|
|
253
|
-
WHERE session_id = ? AND category = 'skill'
|
|
254
|
-
GROUP BY data
|
|
255
|
-
ORDER BY invocations DESC`).all(sessionId);
|
|
256
|
-
}
|
|
257
|
-
// ═══════════════════════════════════════════════════════
|
|
258
|
-
// GROUP 2 — JS Computed (3 metrics)
|
|
259
|
-
// ═══════════════════════════════════════════════════════
|
|
260
|
-
/**
|
|
261
|
-
* #4 Session Mix — percentage of sessions classified as productive.
|
|
262
|
-
* Iterates all sessions and uses #25 (sessionOutcome) to classify each.
|
|
263
|
-
*/
|
|
264
|
-
sessionMix() {
|
|
265
|
-
const sessions = this.db.prepare(`SELECT session_id FROM session_meta`).all();
|
|
266
|
-
if (sessions.length === 0) {
|
|
267
|
-
return { productive: 0, exploratory: 0 };
|
|
268
|
-
}
|
|
269
|
-
let productiveCount = 0;
|
|
270
|
-
for (const s of sessions) {
|
|
271
|
-
if (this.sessionOutcome(s.session_id) === "productive") {
|
|
272
|
-
productiveCount++;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const productivePct = Math.round((100 * productiveCount) / sessions.length);
|
|
276
|
-
return {
|
|
277
|
-
productive: productivePct,
|
|
278
|
-
exploratory: 100 - productivePct,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* #13 / #20 Efficiency Score — composite score (0-100) measuring session productivity.
|
|
283
|
-
*
|
|
284
|
-
* Components:
|
|
285
|
-
* - Error rate (lower = better): weight 30%
|
|
286
|
-
* - Tool diversity (higher = better): weight 20%
|
|
287
|
-
* - Commit presence (boolean bonus): weight 25%
|
|
288
|
-
* - Rework rate (lower = better): weight 15%
|
|
289
|
-
* - Session duration efficiency (moderate = better): weight 10%
|
|
290
|
-
*
|
|
291
|
-
* Formula: score = 100 - errorPenalty + diversityBonus + commitBonus - reworkPenalty + durationBonus - 40
|
|
292
|
-
* The -40 baseline prevents empty sessions from scoring 100.
|
|
293
|
-
*/
|
|
294
|
-
efficiencyScore(sessionId) {
|
|
295
|
-
const errRate = this.errorRate(sessionId);
|
|
296
|
-
const diversity = this.toolDiversity(sessionId);
|
|
297
|
-
const commits = this.commitCount(sessionId);
|
|
298
|
-
const totalEvents = this.db.prepare(`SELECT COUNT(*) as cnt FROM session_events WHERE session_id = ?`).get(sessionId).cnt;
|
|
299
|
-
const fileEvents = this.db.prepare(`SELECT COUNT(*) as cnt FROM session_events WHERE session_id = ? AND category = 'file'`).get(sessionId).cnt;
|
|
300
|
-
// Rework: files edited more than once in this session
|
|
301
|
-
const reworkFiles = this.db.prepare(`SELECT COUNT(*) as cnt FROM (SELECT data, COUNT(*) as edits FROM session_events WHERE session_id = ? AND category = 'file' GROUP BY data HAVING edits > 1)`).get(sessionId);
|
|
302
|
-
const reworkRatio = fileEvents > 0 ? reworkFiles.cnt / fileEvents : 0;
|
|
303
|
-
// Duration in minutes
|
|
304
|
-
const duration = this.sessionDuration(sessionId) ?? 0;
|
|
305
|
-
// Score components
|
|
306
|
-
const errorPenalty = Math.min(errRate * 0.3, 30);
|
|
307
|
-
const diversityBonus = Math.min(diversity * 4, 20);
|
|
308
|
-
const commitBonus = commits > 0 ? 25 : 0;
|
|
309
|
-
const reworkPenalty = Math.min(reworkRatio * 15, 15);
|
|
310
|
-
const durationBonus = duration > 5 && duration < 60 ? 10 : duration >= 60 ? 5 : 0;
|
|
311
|
-
const score = Math.round(Math.max(0, Math.min(100, 100 - errorPenalty + diversityBonus + commitBonus - reworkPenalty + durationBonus - 40)));
|
|
312
|
-
return score;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* #23 Iteration Cycles — counts edit-error-fix sequences in a session.
|
|
316
|
-
*
|
|
317
|
-
* Walks events chronologically and detects patterns where a file event
|
|
318
|
-
* is followed by an error event, then another file event.
|
|
319
|
-
*/
|
|
320
|
-
iterationCycles(sessionId) {
|
|
321
|
-
const events = this.db.prepare(`SELECT category, data FROM session_events WHERE session_id = ? ORDER BY id ASC`).all(sessionId);
|
|
322
|
-
let cycles = 0;
|
|
323
|
-
for (let i = 0; i < events.length - 2; i++) {
|
|
324
|
-
if (events[i].category === "file" &&
|
|
325
|
-
events[i + 1].category === "error" &&
|
|
326
|
-
events[i + 2].category === "file") {
|
|
327
|
-
cycles++;
|
|
328
|
-
i += 2; // Skip past this cycle
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return cycles;
|
|
332
|
-
}
|
|
333
|
-
// ═══════════════════════════════════════════════════════
|
|
334
62
|
// GROUP 3 — Runtime (4 metrics, stubs)
|
|
335
63
|
// ═══════════════════════════════════════════════════════
|
|
336
64
|
/**
|
|
@@ -377,63 +105,11 @@ export class AnalyticsEngine {
|
|
|
377
105
|
return { inputBytes, outputBytes };
|
|
378
106
|
}
|
|
379
107
|
// ═══════════════════════════════════════════════════════
|
|
380
|
-
// GROUP 4 — New Extractor Needed (3 metrics)
|
|
381
|
-
// ═══════════════════════════════════════════════════════
|
|
382
|
-
/**
|
|
383
|
-
* #6 Pattern Detected — identifies recurring patterns in a session.
|
|
384
|
-
*
|
|
385
|
-
* Analyzes category distribution and detects dominant patterns
|
|
386
|
-
* (>60% threshold). Falls back to combination detection and
|
|
387
|
-
* "balanced" for evenly distributed sessions.
|
|
388
|
-
*/
|
|
389
|
-
patternDetected(sessionId) {
|
|
390
|
-
const categories = this.sessionContinuity(sessionId);
|
|
391
|
-
const total = categories.reduce((sum, c) => sum + c.count, 0);
|
|
392
|
-
if (total === 0)
|
|
393
|
-
return "no activity";
|
|
394
|
-
// Sort by count descending
|
|
395
|
-
categories.sort((a, b) => b.count - a.count);
|
|
396
|
-
const dominant = categories[0];
|
|
397
|
-
const ratio = dominant.count / total;
|
|
398
|
-
if (ratio > 0.6) {
|
|
399
|
-
const patterns = {
|
|
400
|
-
file: "heavy file editor",
|
|
401
|
-
git: "git-focused",
|
|
402
|
-
mcp: "tool-heavy",
|
|
403
|
-
error: "debugging session",
|
|
404
|
-
plan: "planning session",
|
|
405
|
-
subagent: "delegation-heavy",
|
|
406
|
-
rule: "configuration session",
|
|
407
|
-
task: "task management",
|
|
408
|
-
};
|
|
409
|
-
return patterns[dominant.category] ?? `${dominant.category}-focused`;
|
|
410
|
-
}
|
|
411
|
-
// Check for common combinations
|
|
412
|
-
if (categories.find((c) => c.category === "git") &&
|
|
413
|
-
categories.find((c) => c.category === "file")) {
|
|
414
|
-
return "build and commit";
|
|
415
|
-
}
|
|
416
|
-
return "balanced";
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* #21 Permission Denials — count of tool calls blocked by security rules.
|
|
420
|
-
*
|
|
421
|
-
* Filters error events containing "denied", "blocked", or "permission".
|
|
422
|
-
* Stub: ideally requires a dedicated extractor in extract.ts.
|
|
423
|
-
*/
|
|
424
|
-
permissionDenials(sessionId) {
|
|
425
|
-
const row = this.db.prepare(`SELECT COUNT(*) as cnt
|
|
426
|
-
FROM session_events
|
|
427
|
-
WHERE session_id = ? AND category = 'error'
|
|
428
|
-
AND (data LIKE '%denied%' OR data LIKE '%blocked%' OR data LIKE '%permission%')`).get(sessionId);
|
|
429
|
-
return row.cnt;
|
|
430
|
-
}
|
|
431
|
-
// ═══════════════════════════════════════════════════════
|
|
432
108
|
// queryAll — single unified report from ONE source
|
|
433
109
|
// ═══════════════════════════════════════════════════════
|
|
434
110
|
/**
|
|
435
|
-
* Build a
|
|
436
|
-
* with
|
|
111
|
+
* Build a FullReport by merging runtime stats (passed in)
|
|
112
|
+
* with continuity data from the DB.
|
|
437
113
|
*
|
|
438
114
|
* This is the ONE call that ctx_stats should use.
|
|
439
115
|
*/
|
|
@@ -476,44 +152,6 @@ export class AnalyticsEngine {
|
|
|
476
152
|
total_savings_ratio: totalSavingsRatio,
|
|
477
153
|
};
|
|
478
154
|
}
|
|
479
|
-
// ── Session metrics ──
|
|
480
|
-
const durationMin = sid ? this.sessionDuration(sid) : null;
|
|
481
|
-
const toolCallsDb = sid ? this.db.prepare("SELECT COUNT(*) as cnt FROM session_events WHERE session_id = ? AND category = 'mcp'").get(sid).cnt : 0;
|
|
482
|
-
// ── Activity metrics ──
|
|
483
|
-
const commits = sid ? this.commitCount(sid) : 0;
|
|
484
|
-
const errors = sid ? this.errorCount(sid) : 0;
|
|
485
|
-
const errorRatePct = sid ? this.errorRate(sid) : 0;
|
|
486
|
-
const toolDiversity = sid ? this.toolDiversity(sid) : 0;
|
|
487
|
-
const effScore = sid ? this.efficiencyScore(sid) : 0;
|
|
488
|
-
const commitsPerSessionAvg = this.commitsPerSession();
|
|
489
|
-
const sessionOutcome = sid ? this.sessionOutcome(sid) : "exploratory";
|
|
490
|
-
const mix = this.sessionMix();
|
|
491
|
-
// ── Pattern metrics ──
|
|
492
|
-
const hourlyRaw = this.hourlyProductivity(sid || undefined);
|
|
493
|
-
const hourlyCommits = Array.from({ length: 24 }, (_, i) => {
|
|
494
|
-
const h = String(i).padStart(2, "0");
|
|
495
|
-
return hourlyRaw.find((r) => r.hour === h)?.count ?? 0;
|
|
496
|
-
});
|
|
497
|
-
const weeklyTrend = this.weeklyTrend();
|
|
498
|
-
const iterCycles = sid ? this.iterationCycles(sid) : 0;
|
|
499
|
-
const rework = sid ? this.reworkRate(sid) : this.reworkRate();
|
|
500
|
-
// ── Health metrics ──
|
|
501
|
-
const claudeMdFreshness = this.claudeMdFreshness().map((r) => {
|
|
502
|
-
const daysAgo = r.last_updated
|
|
503
|
-
? Math.round((Date.now() - new Date(r.last_updated).getTime()) / 86_400_000)
|
|
504
|
-
: null;
|
|
505
|
-
return { project: r.data, days_ago: daysAgo };
|
|
506
|
-
});
|
|
507
|
-
const compactionsThisWeek = sid ? this.compactionCount(sid) : 0;
|
|
508
|
-
const weeklySessions = this.weeklySessionCount();
|
|
509
|
-
const permDenials = sid ? this.permissionDenials(sid) : 0;
|
|
510
|
-
// ── Agent metrics ──
|
|
511
|
-
const subagents = sid
|
|
512
|
-
? this.subagentUsage(sid).map((r) => ({ type: r.data, count: r.total }))
|
|
513
|
-
: [];
|
|
514
|
-
const skills = sid
|
|
515
|
-
? this.skillUsage(sid).map((r) => ({ name: r.data, count: r.invocations }))
|
|
516
|
-
: [];
|
|
517
155
|
// ── Continuity data ──
|
|
518
156
|
const eventTotal = this.db.prepare("SELECT COUNT(*) as cnt FROM session_events").get().cnt;
|
|
519
157
|
const byCategory = this.db.prepare("SELECT category, COUNT(*) as cnt FROM session_events GROUP BY category ORDER BY cnt DESC").all();
|
|
@@ -566,34 +204,8 @@ export class AnalyticsEngine {
|
|
|
566
204
|
cache,
|
|
567
205
|
session: {
|
|
568
206
|
id: sid,
|
|
569
|
-
duration_min: durationMin !== null ? Math.round(durationMin * 10) / 10 : null,
|
|
570
|
-
tool_calls: toolCallsDb,
|
|
571
207
|
uptime_min: uptimeMin,
|
|
572
208
|
},
|
|
573
|
-
activity: {
|
|
574
|
-
commits,
|
|
575
|
-
errors,
|
|
576
|
-
error_rate_pct: errorRatePct,
|
|
577
|
-
tool_diversity: toolDiversity,
|
|
578
|
-
efficiency_score: effScore,
|
|
579
|
-
commits_per_session_avg: commitsPerSessionAvg,
|
|
580
|
-
session_outcome: sessionOutcome,
|
|
581
|
-
productive_pct: mix.productive,
|
|
582
|
-
exploratory_pct: mix.exploratory,
|
|
583
|
-
},
|
|
584
|
-
patterns: {
|
|
585
|
-
hourly_commits: hourlyCommits,
|
|
586
|
-
weekly_trend: weeklyTrend,
|
|
587
|
-
iteration_cycles: iterCycles,
|
|
588
|
-
rework: rework.map((r) => ({ file: r.data, edits: r.edits })),
|
|
589
|
-
},
|
|
590
|
-
health: {
|
|
591
|
-
claude_md_freshness: claudeMdFreshness,
|
|
592
|
-
compactions_this_week: compactionsThisWeek,
|
|
593
|
-
weekly_sessions: weeklySessions,
|
|
594
|
-
permission_denials: permDenials,
|
|
595
|
-
},
|
|
596
|
-
agents: { subagents, skills },
|
|
597
209
|
continuity: {
|
|
598
210
|
total_events: eventTotal,
|
|
599
211
|
by_category: continuityByCategory,
|
|
@@ -604,138 +216,137 @@ export class AnalyticsEngine {
|
|
|
604
216
|
}
|
|
605
217
|
}
|
|
606
218
|
// ─────────────────────────────────────────────────────────
|
|
607
|
-
// formatReport — renders FullReport as
|
|
219
|
+
// formatReport — renders FullReport as concise, honest output
|
|
608
220
|
// ─────────────────────────────────────────────────────────
|
|
609
221
|
/** Format bytes as human-readable KB or MB. */
|
|
610
222
|
function kb(b) {
|
|
611
223
|
if (b >= 1024 * 1024)
|
|
612
|
-
return `${(b / 1024 / 1024).toFixed(1)}MB`;
|
|
613
|
-
|
|
224
|
+
return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
225
|
+
if (b >= 1024)
|
|
226
|
+
return `${(b / 1024).toFixed(1)} KB`;
|
|
227
|
+
return `${b} B`;
|
|
228
|
+
}
|
|
229
|
+
/** Format session uptime as human-readable duration. */
|
|
230
|
+
function formatDuration(uptimeMin) {
|
|
231
|
+
const min = parseFloat(uptimeMin);
|
|
232
|
+
if (isNaN(min) || min < 1)
|
|
233
|
+
return "< 1 min";
|
|
234
|
+
if (min < 60)
|
|
235
|
+
return `${Math.round(min)} min`;
|
|
236
|
+
const h = Math.floor(min / 60);
|
|
237
|
+
const m = Math.round(min % 60);
|
|
238
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
614
239
|
}
|
|
615
240
|
/**
|
|
616
|
-
*
|
|
241
|
+
* Build a before/after comparison bar.
|
|
617
242
|
*
|
|
618
|
-
*
|
|
619
|
-
* The
|
|
243
|
+
* The "without" bar is always full (40 chars).
|
|
244
|
+
* The "with" bar is proportional to the ratio of returned vs total.
|
|
620
245
|
*/
|
|
621
|
-
|
|
246
|
+
function comparisonBars(total, returned) {
|
|
247
|
+
const BAR_WIDTH = 40;
|
|
248
|
+
const withoutBar = "#".repeat(BAR_WIDTH);
|
|
249
|
+
const withFill = total > 0 ? Math.max(1, Math.round((returned / total) * BAR_WIDTH)) : BAR_WIDTH;
|
|
250
|
+
const withBar = "#".repeat(withFill) + " ".repeat(BAR_WIDTH - withFill);
|
|
251
|
+
return { withoutBar, withBar };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Render a FullReport as a before/after comparison developers instantly understand.
|
|
255
|
+
*
|
|
256
|
+
* Design rules:
|
|
257
|
+
* - If no savings, show "fresh session" format (no fake percentages)
|
|
258
|
+
* - Active session shows BEFORE vs AFTER -- what would have flooded your conversation vs what actually did
|
|
259
|
+
* - Per-tool table only if 2+ different tools were called
|
|
260
|
+
* - Time gained is the hero metric
|
|
261
|
+
* - Under 15 lines for typical sessions
|
|
262
|
+
*/
|
|
263
|
+
export function formatReport(report, version, latestVersion) {
|
|
622
264
|
const lines = [];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
265
|
+
const duration = formatDuration(report.session.uptime_min);
|
|
266
|
+
// ── Compute real savings ──
|
|
267
|
+
const totalKeptOut = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
|
|
268
|
+
const totalReturned = report.savings.total_bytes_returned;
|
|
269
|
+
const totalCalls = report.savings.total_calls;
|
|
270
|
+
// ── Fresh session: almost no activity ──
|
|
271
|
+
if (totalKeptOut === 0) {
|
|
272
|
+
lines.push(`context-mode -- session (${duration})`);
|
|
273
|
+
lines.push("");
|
|
274
|
+
if (totalCalls === 0) {
|
|
275
|
+
lines.push("No tool calls yet.");
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const callLabel = totalCalls === 1 ? "1 tool call" : `${totalCalls} tool calls`;
|
|
279
|
+
lines.push(`${callLabel} | ${kb(totalReturned)} in context | no savings yet`);
|
|
280
|
+
}
|
|
281
|
+
lines.push("");
|
|
282
|
+
lines.push("Tip: Use ctx_execute to analyze files in sandbox -- savings start there.");
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push(version ? `v${version}` : "context-mode");
|
|
285
|
+
if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
|
|
286
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | Run: ctx_upgrade`);
|
|
287
|
+
}
|
|
627
288
|
return lines.join("\n");
|
|
628
289
|
}
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
const grandTotal = totalProcessed + totalReturned;
|
|
290
|
+
// ── Active session with real savings ──
|
|
291
|
+
const grandTotal = totalKeptOut + totalReturned;
|
|
632
292
|
const savingsPercent = grandTotal > 0
|
|
633
|
-
? ((
|
|
293
|
+
? ((totalKeptOut / grandTotal) * 100).toFixed(1)
|
|
634
294
|
: "0.0";
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
lines.push(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (fileCount > 0) {
|
|
655
|
-
lines.push(`-> ${fileCount} file${fileCount !== 1 ? "s" : ""} analyzed in sandbox (never entered context)`);
|
|
656
|
-
}
|
|
657
|
-
if (networkCount > 0) {
|
|
658
|
-
lines.push(`-> ${networkCount} API call${networkCount !== 1 ? "s" : ""} sandboxed (responses indexed, not dumped)`);
|
|
659
|
-
}
|
|
660
|
-
if (searchCount > 0) {
|
|
661
|
-
lines.push(`-> ${searchCount} search quer${searchCount !== 1 ? "ies" : "y"} served from index`);
|
|
662
|
-
}
|
|
663
|
-
if (cacheCount > 0) {
|
|
664
|
-
lines.push(`-> ${cacheCount} repeat fetch${cacheCount !== 1 ? "es" : ""} avoided (TTL cache)`);
|
|
665
|
-
}
|
|
666
|
-
// ── Per-tool breakdown ──
|
|
295
|
+
// ── Time saved estimate (hero metric) ──
|
|
296
|
+
// ~4 bytes per token, ~1000 tokens per minute of context window capacity
|
|
297
|
+
const minSaved = Math.round(totalKeptOut / 4 / 1000);
|
|
298
|
+
lines.push(`context-mode -- session (${duration})`);
|
|
299
|
+
lines.push("");
|
|
300
|
+
// ── Before/after comparison ──
|
|
301
|
+
const { withoutBar, withBar } = comparisonBars(grandTotal, totalReturned);
|
|
302
|
+
lines.push(`Without context-mode: |${withoutBar}| ${kb(grandTotal)} in your conversation`);
|
|
303
|
+
lines.push(`With context-mode: |${withBar}| ${kb(totalReturned)} in your conversation`);
|
|
304
|
+
lines.push("");
|
|
305
|
+
const savingsLine = `${kb(totalKeptOut)} processed in sandbox, never entered your conversation. (${savingsPercent}% reduction)`;
|
|
306
|
+
lines.push(savingsLine);
|
|
307
|
+
if (minSaved > 0) {
|
|
308
|
+
const timeSaved = minSaved >= 60
|
|
309
|
+
? `+${Math.floor(minSaved / 60)}h ${minSaved % 60}m`
|
|
310
|
+
: `+${minSaved}m`;
|
|
311
|
+
lines.push(`${timeSaved} session time gained.`);
|
|
312
|
+
}
|
|
313
|
+
// ── Per-tool table (only if 2+ different tools) ──
|
|
667
314
|
const activatedTools = report.savings.by_tool.filter((t) => t.calls > 0);
|
|
668
|
-
if (activatedTools.length
|
|
669
|
-
lines.push(""
|
|
315
|
+
if (activatedTools.length >= 2) {
|
|
316
|
+
lines.push("");
|
|
670
317
|
for (const t of activatedTools) {
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
? (((processed - contextUsed) / processed) *
|
|
675
|
-
100).toFixed(0)
|
|
676
|
-
: "--";
|
|
677
|
-
lines.push(`| ${t.tool} | ${t.calls} | ${kb(processed)} | ${kb(contextUsed)} | ${savedPct}% |`);
|
|
318
|
+
const returned = t.context_kb * 1024;
|
|
319
|
+
const callLabel = `${t.calls} call${t.calls !== 1 ? "s" : ""}`;
|
|
320
|
+
lines.push(` ${t.tool.padEnd(22)} ${callLabel.padEnd(10)} ${kb(returned)} used`);
|
|
678
321
|
}
|
|
679
322
|
}
|
|
680
|
-
// ── Session continuity ──
|
|
681
|
-
if (report.continuity.
|
|
682
|
-
lines.push(""
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (taskRow && taskRow.count > 0) {
|
|
691
|
-
parts.push(`${taskRow.count} task${taskRow.count !== 1 ? "s" : ""} tracked`);
|
|
692
|
-
}
|
|
693
|
-
lines.push(parts.join(" | "));
|
|
694
|
-
if (report.continuity.compact_count > 0) {
|
|
695
|
-
lines.push("", `Session knowledge preserved across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""} -- zero context lost.`);
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
lines.push("", "When your context compacts, all of this will restore awareness -- no starting from scratch.");
|
|
699
|
-
}
|
|
700
|
-
if (report.continuity.resume_ready) {
|
|
701
|
-
lines.push("Resume snapshot ready for the next compaction.");
|
|
323
|
+
// ── Session continuity breakdown ──
|
|
324
|
+
if (report.continuity.by_category.length > 0) {
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push(`Session continuity: ${report.continuity.total_events} events preserved across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
|
|
327
|
+
lines.push("");
|
|
328
|
+
for (const c of report.continuity.by_category) {
|
|
329
|
+
const cat = c.category.padEnd(9);
|
|
330
|
+
const count = String(c.count).padStart(3);
|
|
331
|
+
const preview = c.preview.length > 45 ? c.preview.slice(0, 42) + "..." : c.preview;
|
|
332
|
+
lines.push(` ${cat} ${count} ${preview.padEnd(47)} ${c.why}`);
|
|
702
333
|
}
|
|
703
334
|
}
|
|
704
|
-
// ──
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
hourly_commits: report.patterns.hourly_commits,
|
|
721
|
-
weekly_trend: report.patterns.weekly_trend,
|
|
722
|
-
iteration_cycles: report.patterns.iteration_cycles,
|
|
723
|
-
rework: report.patterns.rework,
|
|
724
|
-
},
|
|
725
|
-
health: {
|
|
726
|
-
claude_md_freshness: report.health.claude_md_freshness,
|
|
727
|
-
compactions_this_week: report.health.compactions_this_week,
|
|
728
|
-
weekly_sessions: report.health.weekly_sessions,
|
|
729
|
-
permission_denials: report.health.permission_denials,
|
|
730
|
-
},
|
|
731
|
-
agents: {
|
|
732
|
-
subagents: report.agents.subagents,
|
|
733
|
-
skills: report.agents.skills,
|
|
734
|
-
},
|
|
735
|
-
continuity: { total_events: report.continuity.total_events },
|
|
736
|
-
};
|
|
737
|
-
lines.push("", "### Analytics (27 metrics)", "", "```json", JSON.stringify(analyticsJson, null, 2), "```");
|
|
738
|
-
// Footer
|
|
739
|
-
lines.push("", "---", "context-mode -- github.com/mksglu/context-mode", "", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
|
|
335
|
+
// ── Footer: version + outdated warning ──
|
|
336
|
+
const footerParts = [];
|
|
337
|
+
if (report.continuity.by_category.length === 0 && report.continuity.compact_count > 0) {
|
|
338
|
+
footerParts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
|
|
339
|
+
}
|
|
340
|
+
if (report.continuity.by_category.length === 0 && report.continuity.total_events > 0) {
|
|
341
|
+
footerParts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
|
|
342
|
+
}
|
|
343
|
+
const versionStr = version ? `v${version}` : "context-mode";
|
|
344
|
+
footerParts.push(versionStr);
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(footerParts.join(" | "));
|
|
347
|
+
// Outdated warning in footer
|
|
348
|
+
if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
|
|
349
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | Run: ctx_upgrade`);
|
|
350
|
+
}
|
|
740
351
|
return lines.join("\n");
|
|
741
352
|
}
|