context-mode 1.0.65 → 1.0.67
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 +3 -1
- package/build/cli.js +28 -0
- package/build/server.js +205 -205
- package/build/session/analytics.d.ts +381 -0
- package/build/session/analytics.js +708 -0
- package/cli.bundle.mjs +208 -148
- package/configs/antigravity/GEMINI.md +3 -0
- package/configs/claude-code/CLAUDE.md +3 -0
- package/configs/codex/AGENTS.md +3 -0
- package/configs/gemini-cli/GEMINI.md +3 -0
- package/configs/kilo/AGENTS.md +3 -0
- package/configs/kiro/KIRO.md +3 -0
- package/configs/openclaw/AGENTS.md +3 -0
- package/configs/opencode/AGENTS.md +3 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +3 -0
- package/hooks/routing-block.mjs +4 -1
- package/hooks/session-helpers.mjs +0 -12
- package/hooks/sessionstart.mjs +2 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +134 -74
- package/skills/context-mode/SKILL.md +2 -0
- package/skills/context-mode-ops/SKILL.md +18 -0
- package/skills/context-mode-ops/release.md +15 -0
- package/skills/ctx-purge/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +6 -0
- package/start.mjs +7 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsEngine — All 27 metrics from SessionDB.
|
|
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
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const engine = new AnalyticsEngine(sessionDb);
|
|
14
|
+
* const report = engine.queryAll(runtimeStats);
|
|
15
|
+
*/
|
|
16
|
+
// ─────────────────────────────────────────────────────────
|
|
17
|
+
// Category labels and hints for session continuity display
|
|
18
|
+
// ─────────────────────────────────────────────────────────
|
|
19
|
+
/** Human-readable labels for event categories. */
|
|
20
|
+
export const categoryLabels = {
|
|
21
|
+
file: "Files tracked",
|
|
22
|
+
rule: "Project rules (CLAUDE.md)",
|
|
23
|
+
prompt: "Your requests saved",
|
|
24
|
+
mcp: "Plugin tools used",
|
|
25
|
+
git: "Git operations",
|
|
26
|
+
env: "Environment setup",
|
|
27
|
+
error: "Errors caught",
|
|
28
|
+
task: "Tasks in progress",
|
|
29
|
+
decision: "Your decisions",
|
|
30
|
+
cwd: "Working directory",
|
|
31
|
+
skill: "Skills used",
|
|
32
|
+
subagent: "Delegated work",
|
|
33
|
+
intent: "Session mode",
|
|
34
|
+
data: "Data references",
|
|
35
|
+
role: "Behavioral directives",
|
|
36
|
+
};
|
|
37
|
+
/** Explains why each category matters for continuity. */
|
|
38
|
+
export const categoryHints = {
|
|
39
|
+
file: "Restored after compact \u2014 no need to re-read",
|
|
40
|
+
rule: "Your project instructions survive context resets",
|
|
41
|
+
prompt: "Continues exactly where you left off",
|
|
42
|
+
decision: "Applied automatically \u2014 won\u2019t ask again",
|
|
43
|
+
task: "Picks up from where it stopped",
|
|
44
|
+
error: "Tracked and monitored across compacts",
|
|
45
|
+
git: "Branch, commit, and repo state preserved",
|
|
46
|
+
env: "Runtime config carried forward",
|
|
47
|
+
mcp: "Tool usage patterns remembered",
|
|
48
|
+
subagent: "Delegation history preserved",
|
|
49
|
+
skill: "Skill invocations tracked",
|
|
50
|
+
};
|
|
51
|
+
// ─────────────────────────────────────────────────────────
|
|
52
|
+
// AnalyticsEngine
|
|
53
|
+
// ─────────────────────────────────────────────────────────
|
|
54
|
+
export class AnalyticsEngine {
|
|
55
|
+
db;
|
|
56
|
+
/**
|
|
57
|
+
* Create an AnalyticsEngine.
|
|
58
|
+
*
|
|
59
|
+
* Accepts either a SessionDB instance (extracts internal db via
|
|
60
|
+
* the protected getter — use the static fromDB helper for raw adapters)
|
|
61
|
+
* or any object with a prepare() method for direct usage.
|
|
62
|
+
*/
|
|
63
|
+
constructor(db) {
|
|
64
|
+
this.db = db;
|
|
65
|
+
}
|
|
66
|
+
// ═══════════════════════════════════════════════════════
|
|
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
|
+
// GROUP 3 — Runtime (4 metrics, stubs)
|
|
335
|
+
// ═══════════════════════════════════════════════════════
|
|
336
|
+
/**
|
|
337
|
+
* #1 Context Savings Total — bytes kept out of context window.
|
|
338
|
+
*
|
|
339
|
+
* Stub: requires server.ts to accumulate rawBytes and contextBytes
|
|
340
|
+
* during a live session. Call with tracked values.
|
|
341
|
+
*/
|
|
342
|
+
static contextSavingsTotal(rawBytes, contextBytes) {
|
|
343
|
+
const savedBytes = rawBytes - contextBytes;
|
|
344
|
+
const savedPercent = rawBytes > 0
|
|
345
|
+
? Math.round((savedBytes / rawBytes) * 1000) / 10
|
|
346
|
+
: 0;
|
|
347
|
+
return { rawBytes, contextBytes, savedBytes, savedPercent };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* #2 Think in Code Comparison — ratio of file size to sandbox output size.
|
|
351
|
+
*
|
|
352
|
+
* Stub: requires server.ts tracking of execute/execute_file calls.
|
|
353
|
+
*/
|
|
354
|
+
static thinkInCodeComparison(fileBytes, outputBytes) {
|
|
355
|
+
const ratio = outputBytes > 0
|
|
356
|
+
? Math.round((fileBytes / outputBytes) * 10) / 10
|
|
357
|
+
: 0;
|
|
358
|
+
return { fileBytes, outputBytes, ratio };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* #3 Tool Savings — per-tool breakdown of context savings.
|
|
362
|
+
*
|
|
363
|
+
* Stub: requires per-tool accumulators in server.ts.
|
|
364
|
+
*/
|
|
365
|
+
static toolSavings(tools) {
|
|
366
|
+
return tools.map((t) => ({
|
|
367
|
+
...t,
|
|
368
|
+
savedBytes: t.rawBytes - t.contextBytes,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* #19 Sandbox I/O — total input/output bytes processed by the sandbox.
|
|
373
|
+
*
|
|
374
|
+
* Stub: requires PolyglotExecutor byte counters.
|
|
375
|
+
*/
|
|
376
|
+
static sandboxIO(inputBytes, outputBytes) {
|
|
377
|
+
return { inputBytes, outputBytes };
|
|
378
|
+
}
|
|
379
|
+
// ═══════════════════════════════════════════════════════
|
|
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
|
+
// queryAll — single unified report from ONE source
|
|
433
|
+
// ═══════════════════════════════════════════════════════
|
|
434
|
+
/**
|
|
435
|
+
* Build a complete FullReport by merging runtime stats (passed in)
|
|
436
|
+
* with all 27 DB-backed metrics and continuity data.
|
|
437
|
+
*
|
|
438
|
+
* This is the ONE call that ctx_stats should use.
|
|
439
|
+
*/
|
|
440
|
+
queryAll(runtimeStats) {
|
|
441
|
+
// ── Resolve latest session ID ──
|
|
442
|
+
const latestSession = this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get();
|
|
443
|
+
const sid = latestSession?.session_id ?? "";
|
|
444
|
+
// ── Runtime savings ──
|
|
445
|
+
const totalBytesReturned = Object.values(runtimeStats.bytesReturned).reduce((sum, b) => sum + b, 0);
|
|
446
|
+
const totalCalls = Object.values(runtimeStats.calls).reduce((sum, c) => sum + c, 0);
|
|
447
|
+
const keptOut = runtimeStats.bytesIndexed + runtimeStats.bytesSandboxed;
|
|
448
|
+
const totalProcessed = keptOut + totalBytesReturned;
|
|
449
|
+
const savingsRatio = totalProcessed / Math.max(totalBytesReturned, 1);
|
|
450
|
+
const reductionPct = totalProcessed > 0
|
|
451
|
+
? Math.round((1 - totalBytesReturned / totalProcessed) * 100)
|
|
452
|
+
: 0;
|
|
453
|
+
const toolNames = new Set([
|
|
454
|
+
...Object.keys(runtimeStats.calls),
|
|
455
|
+
...Object.keys(runtimeStats.bytesReturned),
|
|
456
|
+
]);
|
|
457
|
+
const byTool = Array.from(toolNames).sort().map((tool) => ({
|
|
458
|
+
tool,
|
|
459
|
+
calls: runtimeStats.calls[tool] || 0,
|
|
460
|
+
context_kb: Math.round((runtimeStats.bytesReturned[tool] || 0) / 1024 * 10) / 10,
|
|
461
|
+
tokens: Math.round((runtimeStats.bytesReturned[tool] || 0) / 4),
|
|
462
|
+
}));
|
|
463
|
+
const uptimeMs = Date.now() - runtimeStats.sessionStart;
|
|
464
|
+
const uptimeMin = (uptimeMs / 60_000).toFixed(1);
|
|
465
|
+
// ── Cache ──
|
|
466
|
+
let cache;
|
|
467
|
+
if (runtimeStats.cacheHits > 0 || runtimeStats.cacheBytesSaved > 0) {
|
|
468
|
+
const totalWithCache = totalProcessed + runtimeStats.cacheBytesSaved;
|
|
469
|
+
const totalSavingsRatio = totalWithCache / Math.max(totalBytesReturned, 1);
|
|
470
|
+
const ttlHoursLeft = Math.max(0, 24 - Math.floor((Date.now() - runtimeStats.sessionStart) / (60 * 60 * 1000)));
|
|
471
|
+
cache = {
|
|
472
|
+
hits: runtimeStats.cacheHits,
|
|
473
|
+
bytes_saved: runtimeStats.cacheBytesSaved,
|
|
474
|
+
ttl_hours_left: ttlHoursLeft,
|
|
475
|
+
total_with_cache: totalWithCache,
|
|
476
|
+
total_savings_ratio: totalSavingsRatio,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
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
|
+
// ── Continuity data ──
|
|
518
|
+
const eventTotal = this.db.prepare("SELECT COUNT(*) as cnt FROM session_events").get().cnt;
|
|
519
|
+
const byCategory = this.db.prepare("SELECT category, COUNT(*) as cnt FROM session_events GROUP BY category ORDER BY cnt DESC").all();
|
|
520
|
+
const meta = this.db.prepare("SELECT compact_count FROM session_meta ORDER BY started_at DESC LIMIT 1").get();
|
|
521
|
+
const compactCount = meta?.compact_count ?? 0;
|
|
522
|
+
const resume = this.db.prepare("SELECT event_count, consumed FROM session_resume ORDER BY created_at DESC LIMIT 1").get();
|
|
523
|
+
const resumeReady = resume ? !resume.consumed : false;
|
|
524
|
+
// Build category previews
|
|
525
|
+
const previewRows = this.db.prepare("SELECT category, type, data FROM session_events ORDER BY id DESC").all();
|
|
526
|
+
const previews = new Map();
|
|
527
|
+
for (const row of previewRows) {
|
|
528
|
+
if (!previews.has(row.category))
|
|
529
|
+
previews.set(row.category, new Set());
|
|
530
|
+
const set = previews.get(row.category);
|
|
531
|
+
if (set.size < 5) {
|
|
532
|
+
let display = row.data;
|
|
533
|
+
if (row.category === "file") {
|
|
534
|
+
display = row.data.split("/").pop() || row.data;
|
|
535
|
+
}
|
|
536
|
+
else if (row.category === "prompt") {
|
|
537
|
+
display = display.length > 50 ? display.slice(0, 47) + "..." : display;
|
|
538
|
+
}
|
|
539
|
+
if (display.length > 40)
|
|
540
|
+
display = display.slice(0, 37) + "...";
|
|
541
|
+
set.add(display);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const continuityByCategory = byCategory.map((row) => ({
|
|
545
|
+
category: row.category,
|
|
546
|
+
count: row.cnt,
|
|
547
|
+
label: categoryLabels[row.category] || row.category,
|
|
548
|
+
preview: previews.get(row.category)
|
|
549
|
+
? Array.from(previews.get(row.category)).join(", ")
|
|
550
|
+
: "",
|
|
551
|
+
why: categoryHints[row.category] || "Survives context resets",
|
|
552
|
+
}));
|
|
553
|
+
return {
|
|
554
|
+
savings: {
|
|
555
|
+
processed_kb: Math.round(totalProcessed / 1024 * 10) / 10,
|
|
556
|
+
entered_kb: Math.round(totalBytesReturned / 1024 * 10) / 10,
|
|
557
|
+
saved_kb: Math.round(keptOut / 1024 * 10) / 10,
|
|
558
|
+
pct: reductionPct,
|
|
559
|
+
savings_ratio: Math.round(savingsRatio * 10) / 10,
|
|
560
|
+
by_tool: byTool,
|
|
561
|
+
total_calls: totalCalls,
|
|
562
|
+
total_bytes_returned: totalBytesReturned,
|
|
563
|
+
kept_out: keptOut,
|
|
564
|
+
total_processed: totalProcessed,
|
|
565
|
+
},
|
|
566
|
+
cache,
|
|
567
|
+
session: {
|
|
568
|
+
id: sid,
|
|
569
|
+
duration_min: durationMin !== null ? Math.round(durationMin * 10) / 10 : null,
|
|
570
|
+
tool_calls: toolCallsDb,
|
|
571
|
+
uptime_min: uptimeMin,
|
|
572
|
+
},
|
|
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
|
+
continuity: {
|
|
598
|
+
total_events: eventTotal,
|
|
599
|
+
by_category: continuityByCategory,
|
|
600
|
+
compact_count: compactCount,
|
|
601
|
+
resume_ready: resumeReady,
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// ─────────────────────────────────────────────────────────
|
|
607
|
+
// formatReport — renders FullReport as markdown
|
|
608
|
+
// ─────────────────────────────────────────────────────────
|
|
609
|
+
/** Format bytes as human-readable KB or MB. */
|
|
610
|
+
function kb(b) {
|
|
611
|
+
if (b >= 1024 * 1024)
|
|
612
|
+
return `${(b / 1024 / 1024).toFixed(1)}MB`;
|
|
613
|
+
return `${(b / 1024).toFixed(1)}KB`;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Render a FullReport as the same markdown output ctx_stats has always produced.
|
|
617
|
+
*
|
|
618
|
+
* Preserves the exact output format: Context Window Protection table,
|
|
619
|
+
* TTL Cache section, Session Continuity table, and Analytics JSON block.
|
|
620
|
+
*/
|
|
621
|
+
export function formatReport(report) {
|
|
622
|
+
const lines = [
|
|
623
|
+
`## context-mode \u2014 Session Report (${report.session.uptime_min} min)`,
|
|
624
|
+
];
|
|
625
|
+
// ── Feature 1: Context Window Protection ──
|
|
626
|
+
lines.push("", `### Context Window Protection`, "");
|
|
627
|
+
if (report.savings.total_calls === 0) {
|
|
628
|
+
lines.push(`No context-mode tool calls yet. Use \`batch_execute\`, \`execute\`, or \`fetch_and_index\` to keep raw output out of your context window.`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
lines.push(`| Metric | Value |`, `|--------|------:|`, `| Total data processed | **${kb(report.savings.total_processed)}** |`, `| Kept in sandbox (never entered context) | **${kb(report.savings.kept_out)}** |`, `| Entered context | ${kb(report.savings.total_bytes_returned)} |`, `| Estimated tokens saved | ~${Math.round(report.savings.kept_out / 4).toLocaleString()} |`, `| **Context savings** | **${report.savings.savings_ratio.toFixed(1)}x (${report.savings.pct}% reduction)** |`);
|
|
632
|
+
// Per-tool breakdown
|
|
633
|
+
if (report.savings.by_tool.length > 0) {
|
|
634
|
+
lines.push("", `| Tool | Calls | Context | Tokens |`, `|------|------:|--------:|-------:|`);
|
|
635
|
+
for (const t of report.savings.by_tool) {
|
|
636
|
+
lines.push(`| ${t.tool} | ${t.calls} | ${kb(t.calls > 0 ? (t.tokens * 4) : 0)} | ~${t.tokens.toLocaleString()} |`);
|
|
637
|
+
}
|
|
638
|
+
lines.push(`| **Total** | **${report.savings.total_calls}** | **${kb(report.savings.total_bytes_returned)}** | **~${Math.round(report.savings.total_bytes_returned / 4).toLocaleString()}** |`);
|
|
639
|
+
}
|
|
640
|
+
if (report.savings.kept_out > 0) {
|
|
641
|
+
lines.push("", `Without context-mode, **${kb(report.savings.total_processed)}** of raw output would flood your context window. Instead, **${report.savings.pct}%** stayed in sandbox.`);
|
|
642
|
+
}
|
|
643
|
+
// Cache savings section
|
|
644
|
+
if (report.cache) {
|
|
645
|
+
lines.push("", `### TTL Cache`, "", `| Metric | Value |`, `|--------|------:|`, `| Cache hits | **${report.cache.hits}** |`, `| Data avoided by cache | **${kb(report.cache.bytes_saved)}** |`, `| Network requests saved | **${report.cache.hits}** |`, `| TTL remaining | **~${report.cache.ttl_hours_left}h** |`, "", `Content was already indexed in the knowledge base \u2014 ${report.cache.hits} fetch${report.cache.hits > 1 ? "es" : ""} skipped entirely. **${kb(report.cache.bytes_saved)}** of network I/O avoided. Search results served directly from local FTS5 index.`);
|
|
646
|
+
if (report.cache.total_savings_ratio > report.savings.savings_ratio) {
|
|
647
|
+
lines.push("", `**Total context savings (sandbox + cache): ${report.cache.total_savings_ratio.toFixed(1)}x** \u2014 ${kb(report.cache.total_with_cache)} processed, only ${kb(report.savings.total_bytes_returned)} entered context.`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ── Session Continuity ──
|
|
652
|
+
if (report.continuity.total_events > 0) {
|
|
653
|
+
lines.push("", "### Session Continuity", "", "| What's preserved | Count | I remember... | Why it matters |", "|------------------|------:|---------------|----------------|");
|
|
654
|
+
for (const row of report.continuity.by_category) {
|
|
655
|
+
lines.push(`| ${row.label} | ${row.count} | ${row.preview} | ${row.why} |`);
|
|
656
|
+
}
|
|
657
|
+
lines.push(`| **Total** | **${report.continuity.total_events}** | | **Zero knowledge lost on compact** |`);
|
|
658
|
+
lines.push("");
|
|
659
|
+
if (report.continuity.compact_count > 0) {
|
|
660
|
+
lines.push(`Context has been compacted **${report.continuity.compact_count} time(s)** \u2014 session knowledge was preserved each time.`);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
lines.push(`When your context compacts, all of this will restore Claude's awareness \u2014 no starting from scratch.`);
|
|
664
|
+
}
|
|
665
|
+
if (report.continuity.resume_ready) {
|
|
666
|
+
lines.push(`Resume snapshot ready for the next compaction.`);
|
|
667
|
+
}
|
|
668
|
+
lines.push("");
|
|
669
|
+
lines.push(`> **Note:** Previous session data is loaded when you start a new session. Without \`--continue\`, old session history is cleaned up to keep the database lean.`);
|
|
670
|
+
}
|
|
671
|
+
// ── Analytics JSON ──
|
|
672
|
+
const analyticsJson = {
|
|
673
|
+
session: {
|
|
674
|
+
duration_min: report.session.duration_min,
|
|
675
|
+
tool_calls: report.session.tool_calls,
|
|
676
|
+
},
|
|
677
|
+
activity: {
|
|
678
|
+
commits: report.activity.commits,
|
|
679
|
+
errors: report.activity.errors,
|
|
680
|
+
error_rate_pct: report.activity.error_rate_pct,
|
|
681
|
+
tool_diversity: report.activity.tool_diversity,
|
|
682
|
+
efficiency_score: report.activity.efficiency_score,
|
|
683
|
+
commits_per_session_avg: report.activity.commits_per_session_avg,
|
|
684
|
+
session_outcome: report.activity.session_outcome,
|
|
685
|
+
},
|
|
686
|
+
patterns: {
|
|
687
|
+
hourly_commits: report.patterns.hourly_commits,
|
|
688
|
+
weekly_trend: report.patterns.weekly_trend,
|
|
689
|
+
iteration_cycles: report.patterns.iteration_cycles,
|
|
690
|
+
rework: report.patterns.rework,
|
|
691
|
+
},
|
|
692
|
+
health: {
|
|
693
|
+
claude_md_freshness: report.health.claude_md_freshness,
|
|
694
|
+
compactions_this_week: report.health.compactions_this_week,
|
|
695
|
+
weekly_sessions: report.health.weekly_sessions,
|
|
696
|
+
permission_denials: report.health.permission_denials,
|
|
697
|
+
},
|
|
698
|
+
agents: {
|
|
699
|
+
subagents: report.agents.subagents,
|
|
700
|
+
skills: report.agents.skills,
|
|
701
|
+
},
|
|
702
|
+
continuity: { total_events: report.continuity.total_events },
|
|
703
|
+
};
|
|
704
|
+
lines.push("", "### Analytics (27 metrics)", "", "```json", JSON.stringify(analyticsJson, null, 2), "```");
|
|
705
|
+
// Formatting directive
|
|
706
|
+
lines.push("", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
|
|
707
|
+
return lines.join("\n");
|
|
708
|
+
}
|