context-mode 1.0.71 → 1.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,8 @@
1
1
  /**
2
- * AnalyticsEngine — All 27 metrics from SessionDB.
2
+ * AnalyticsEngine — Runtime savings + session continuity reporting.
3
3
  *
4
- * Computes session-level and cross-session analytics using SQL queries
5
- * and JavaScript post-processing. Groups:
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 complete FullReport by merging runtime stats (passed in)
436
- * with all 27 DB-backed metrics and continuity data.
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,125 @@ export class AnalyticsEngine {
604
216
  }
605
217
  }
606
218
  // ─────────────────────────────────────────────────────────
607
- // formatReport — renders FullReport as markdown
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
- return `${(b / 1024).toFixed(1)}KB`;
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
- * Render a FullReport as a marketing-friendly, outcome-focused session story.
241
+ * Build a before/after comparison bar.
617
242
  *
618
- * Framework: Persona -> Metric -> Evidence -> Action -> ROI
619
- * The output tells a narrative instead of dumping raw numbers.
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
- export function formatReport(report) {
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
- // ── Hero: headline story ──
624
- lines.push(`## Think in Code -- Session Report`);
625
- if (report.savings.total_calls === 0) {
626
- lines.push("", "No sandbox activity yet. Use ctx_execute or ctx_batch_execute to start saving context.", "", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
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
- const totalProcessed = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
630
- const totalReturned = report.savings.total_bytes_returned;
631
- const grandTotal = totalProcessed + totalReturned;
290
+ // ── Active session with real savings ──
291
+ const grandTotal = totalKeptOut + totalReturned;
632
292
  const savingsPercent = grandTotal > 0
633
- ? ((1 - totalReturned / grandTotal) * 100).toFixed(1)
293
+ ? ((totalKeptOut / grandTotal) * 100).toFixed(1)
634
294
  : "0.0";
635
- // Rough estimate: 4 bytes per token, ~2 min reading time per 1000 tokens
636
- const tokensSaved = Math.round(totalProcessed / 4);
637
- const extraMinutes = Math.round((tokensSaved / 1000) * 2);
638
- lines.push("", `Your agent processed ${kb(totalProcessed)} of data.`, `Only ${kb(totalReturned)} entered your context window.`, "", `**Context saved: ${savingsPercent}% -- session extended by ~${extraMinutes} minutes**`);
639
- // ── What happened ──
640
- lines.push("", "### What happened", "");
641
- // Count per-tool categories
642
- const toolCallMap = new Map();
643
- for (const t of report.savings.by_tool) {
644
- toolCallMap.set(t.tool, t.calls);
645
- }
646
- const executeCount = (toolCallMap.get("ctx_execute") ?? 0) +
647
- (toolCallMap.get("ctx_execute_file") ?? 0);
648
- const batchCount = toolCallMap.get("ctx_batch_execute") ?? 0;
649
- const searchCount = toolCallMap.get("ctx_search") ?? 0;
650
- const fetchCount = toolCallMap.get("ctx_fetch_and_index") ?? 0;
651
- const fileCount = executeCount + batchCount;
652
- const networkCount = fetchCount;
653
- const cacheCount = report.cache ? report.cache.hits : 0;
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 > 0) {
669
- lines.push("", "### Per-tool breakdown", "", "| Tool | Calls | Data processed | Context used | Saved |", "|------|------:|---------------:|-------------:|------:|");
315
+ if (activatedTools.length >= 2) {
316
+ lines.push("");
670
317
  for (const t of activatedTools) {
671
- const processed = t.tokens * 4; // bytes approximation from tokens
672
- const contextUsed = t.context_kb * 1024;
673
- const savedPct = processed > 0
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 ──
323
+ // ── Footer: continuity + version + outdated warning ──
324
+ const footerParts = [];
325
+ if (report.continuity.compact_count > 0) {
326
+ footerParts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
327
+ }
681
328
  if (report.continuity.total_events > 0) {
682
- lines.push("", "### Session continuity", "");
683
- const parts = [];
684
- if (report.continuity.compact_count > 0) {
685
- parts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
686
- }
687
- parts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
688
- // Count tasks from continuity categories
689
- const taskRow = report.continuity.by_category.find((c) => c.category === "task" || c.category === "tasks");
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.");
702
- }
329
+ footerParts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
330
+ }
331
+ const versionStr = version ? `v${version}` : "context-mode";
332
+ footerParts.push(versionStr);
333
+ lines.push("");
334
+ lines.push(footerParts.join(" | "));
335
+ // Outdated warning in footer
336
+ if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
337
+ lines.push(`Update available: v${version} -> v${latestVersion} | Run: ctx_upgrade`);
703
338
  }
704
- // ── Analytics JSON (for power users) ──
705
- const analyticsJson = {
706
- session: {
707
- duration_min: report.session.duration_min,
708
- tool_calls: report.session.tool_calls,
709
- },
710
- activity: {
711
- commits: report.activity.commits,
712
- errors: report.activity.errors,
713
- error_rate_pct: report.activity.error_rate_pct,
714
- tool_diversity: report.activity.tool_diversity,
715
- efficiency_score: report.activity.efficiency_score,
716
- commits_per_session_avg: report.activity.commits_per_session_avg,
717
- session_outcome: report.activity.session_outcome,
718
- },
719
- patterns: {
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._");
740
339
  return lines.join("\n");
741
340
  }