agentboss 0.1.0
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/README.md +34 -0
- package/bin/aboss.js +288 -0
- package/client/dist/assets/index-C1wFD_Vo.css +1 -0
- package/client/dist/assets/index-DBj1Ujlx.js +137 -0
- package/client/dist/index.html +34 -0
- package/package.json +64 -0
- package/server/analysis/daily-aggregator.js +258 -0
- package/server/analysis/difficulty.js +129 -0
- package/server/analysis/dimensions/ai-knowledge.js +172 -0
- package/server/analysis/dimensions/ai-tools.js +161 -0
- package/server/analysis/dimensions/judgement.js +107 -0
- package/server/analysis/dimensions/llm-merge.js +57 -0
- package/server/analysis/dimensions/output-quality.js +167 -0
- package/server/analysis/dimensions/problem-definition.js +104 -0
- package/server/analysis/dimensions/system-thinking.js +225 -0
- package/server/analysis/evidence-builder.js +104 -0
- package/server/analysis/job.js +273 -0
- package/server/analysis/report-builder.js +581 -0
- package/server/analysis/scoring-v2.js +72 -0
- package/server/analysis/text-signals.js +179 -0
- package/server/analysis/thresholds-v2.js +358 -0
- package/server/api/advice.js +124 -0
- package/server/api/analysis.js +141 -0
- package/server/api/execution.js +330 -0
- package/server/api/metrics.js +277 -0
- package/server/api/overview.js +308 -0
- package/server/api/project.js +255 -0
- package/server/api/reports.js +125 -0
- package/server/api/sessions.js +118 -0
- package/server/api/settings.js +119 -0
- package/server/db/connection.js +175 -0
- package/server/db/queries.js +1051 -0
- package/server/db/schema.js +487 -0
- package/server/etl/active-time.js +150 -0
- package/server/etl/backfill-subagents.js +178 -0
- package/server/etl/claude-code.js +826 -0
- package/server/etl/detect.js +341 -0
- package/server/etl/judge-filter.js +117 -0
- package/server/etl/opencode.js +606 -0
- package/server/execution/job.js +662 -0
- package/server/execution/prompt.js +227 -0
- package/server/execution/runner.js +218 -0
- package/server/index.js +94 -0
- package/server/llm/advice-prompt.js +339 -0
- package/server/llm/advice.js +384 -0
- package/server/llm/analysis-prompt.js +162 -0
- package/server/llm/cli-runner.js +249 -0
- package/server/llm/judge-prompts.js +179 -0
- package/server/llm/judge.js +118 -0
- package/server/llm/project-advice-prompt.js +332 -0
- package/server/llm/project-advice.js +491 -0
- package/server/llm/session-analyzer.js +122 -0
- package/server/utils/project.js +80 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query helpers for Agent Boss (boss.db)
|
|
3
|
+
* Wraps common operations for the sql.js-backed SQLite database.
|
|
4
|
+
*
|
|
5
|
+
* sql.js API notes:
|
|
6
|
+
* - db.run(sql, params) – execute with optional params array
|
|
7
|
+
* - db.exec(sql) – execute, returns [{columns, values}]
|
|
8
|
+
* - db.prepare(sql) – returns statement; use .bind().step() / .getAsObject()
|
|
9
|
+
* - Positional ? placeholders; params passed as arrays
|
|
10
|
+
*
|
|
11
|
+
* @author Felix
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Internal helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a SELECT and return all matching rows as plain objects.
|
|
20
|
+
* @param {Object} db sql.js Database instance
|
|
21
|
+
* @param {string} sql SQL query
|
|
22
|
+
* @param {Array} params Positional parameters
|
|
23
|
+
* @returns {Object[]}
|
|
24
|
+
*/
|
|
25
|
+
function queryAll(db, sql, params = []) {
|
|
26
|
+
const stmt = db.prepare(sql);
|
|
27
|
+
if (params.length) stmt.bind(params);
|
|
28
|
+
const results = [];
|
|
29
|
+
while (stmt.step()) {
|
|
30
|
+
results.push(stmt.getAsObject());
|
|
31
|
+
}
|
|
32
|
+
stmt.free();
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute a SELECT and return the first matching row, or null.
|
|
38
|
+
* @param {Object} db sql.js Database instance
|
|
39
|
+
* @param {string} sql SQL query
|
|
40
|
+
* @param {Array} params Positional parameters
|
|
41
|
+
* @returns {Object|null}
|
|
42
|
+
*/
|
|
43
|
+
function queryOne(db, sql, params = []) {
|
|
44
|
+
const results = queryAll(db, sql, params);
|
|
45
|
+
return results[0] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Session queries
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Insert or replace a unified session row.
|
|
54
|
+
* @param {Object} db
|
|
55
|
+
* @param {Object} session
|
|
56
|
+
*/
|
|
57
|
+
function upsertSession(db, session) {
|
|
58
|
+
const sql = `INSERT OR REPLACE INTO unified_session (
|
|
59
|
+
id, source, date, started_at, ended_at, duration_minutes, active_minutes,
|
|
60
|
+
message_count, tokens_input, tokens_output, tokens_reasoning,
|
|
61
|
+
tokens_cache_read, tokens_cache_write, cost_usd, project, title, model,
|
|
62
|
+
error_count, tool_call_count, summary_additions, summary_deletions,
|
|
63
|
+
summary_files, reverted, time_compacting,
|
|
64
|
+
parent_session_id, agent_type
|
|
65
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
|
|
66
|
+
|
|
67
|
+
db.run(sql, [
|
|
68
|
+
session.id,
|
|
69
|
+
session.source,
|
|
70
|
+
session.date,
|
|
71
|
+
session.started_at,
|
|
72
|
+
session.ended_at || null,
|
|
73
|
+
session.duration_minutes || 0,
|
|
74
|
+
session.active_minutes ?? null,
|
|
75
|
+
session.message_count || 0,
|
|
76
|
+
session.tokens_input || 0,
|
|
77
|
+
session.tokens_output || 0,
|
|
78
|
+
session.tokens_reasoning || 0,
|
|
79
|
+
session.tokens_cache_read || 0,
|
|
80
|
+
session.tokens_cache_write || 0,
|
|
81
|
+
session.cost_usd || 0,
|
|
82
|
+
session.project || null,
|
|
83
|
+
session.title || null,
|
|
84
|
+
session.model || null,
|
|
85
|
+
session.error_count || 0,
|
|
86
|
+
session.tool_call_count || 0,
|
|
87
|
+
session.summary_additions || 0,
|
|
88
|
+
session.summary_deletions || 0,
|
|
89
|
+
session.summary_files || 0,
|
|
90
|
+
session.reverted || 0,
|
|
91
|
+
session.time_compacting || 0,
|
|
92
|
+
session.parent_session_id || null,
|
|
93
|
+
session.agent_type || null,
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get sessions for a specific date, optionally filtered by source.
|
|
99
|
+
* @param {Object} db
|
|
100
|
+
* @param {string} date YYYY-MM-DD
|
|
101
|
+
* @param {string} [source]
|
|
102
|
+
* @returns {Object[]}
|
|
103
|
+
*/
|
|
104
|
+
function getSessionsByDate(db, date, source) {
|
|
105
|
+
if (source) {
|
|
106
|
+
return queryAll(
|
|
107
|
+
db,
|
|
108
|
+
'SELECT * FROM unified_session WHERE date = ? AND source = ? ORDER BY started_at',
|
|
109
|
+
[date, source]
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return queryAll(
|
|
113
|
+
db,
|
|
114
|
+
'SELECT * FROM unified_session WHERE date = ? ORDER BY started_at',
|
|
115
|
+
[date]
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get a single session by ID.
|
|
121
|
+
* @param {Object} db
|
|
122
|
+
* @param {string} id
|
|
123
|
+
* @returns {Object|null}
|
|
124
|
+
*/
|
|
125
|
+
function getSessionById(db, id) {
|
|
126
|
+
return queryOne(db, 'SELECT * FROM unified_session WHERE id = ?', [id]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get sessions in a date range with optional source filter and pagination.
|
|
131
|
+
* @param {Object} db
|
|
132
|
+
* @param {string} fromDate YYYY-MM-DD inclusive
|
|
133
|
+
* @param {string} toDate YYYY-MM-DD inclusive
|
|
134
|
+
* @param {string} [source]
|
|
135
|
+
* @param {number} [limit=100]
|
|
136
|
+
* @param {number} [offset=0]
|
|
137
|
+
* @returns {Object[]}
|
|
138
|
+
*/
|
|
139
|
+
function getSessionsByDateRange(db, fromDate, toDate, source, limit = 100, offset = 0) {
|
|
140
|
+
if (source) {
|
|
141
|
+
return queryAll(
|
|
142
|
+
db,
|
|
143
|
+
`SELECT * FROM unified_session
|
|
144
|
+
WHERE date >= ? AND date <= ? AND source = ?
|
|
145
|
+
ORDER BY started_at DESC
|
|
146
|
+
LIMIT ? OFFSET ?`,
|
|
147
|
+
[fromDate, toDate, source, limit, offset]
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return queryAll(
|
|
151
|
+
db,
|
|
152
|
+
`SELECT * FROM unified_session
|
|
153
|
+
WHERE date >= ? AND date <= ?
|
|
154
|
+
ORDER BY started_at DESC
|
|
155
|
+
LIMIT ? OFFSET ?`,
|
|
156
|
+
[fromDate, toDate, limit, offset]
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Count sessions in a date range (for pagination).
|
|
162
|
+
* @param {Object} db
|
|
163
|
+
* @param {string} fromDate
|
|
164
|
+
* @param {string} toDate
|
|
165
|
+
* @param {string} [source]
|
|
166
|
+
* @returns {number}
|
|
167
|
+
*/
|
|
168
|
+
function countSessionsByDateRange(db, fromDate, toDate, source) {
|
|
169
|
+
const row = source
|
|
170
|
+
? queryOne(
|
|
171
|
+
db,
|
|
172
|
+
'SELECT COUNT(*) AS cnt FROM unified_session WHERE date >= ? AND date <= ? AND source = ?',
|
|
173
|
+
[fromDate, toDate, source]
|
|
174
|
+
)
|
|
175
|
+
: queryOne(
|
|
176
|
+
db,
|
|
177
|
+
'SELECT COUNT(*) AS cnt FROM unified_session WHERE date >= ? AND date <= ?',
|
|
178
|
+
[fromDate, toDate]
|
|
179
|
+
);
|
|
180
|
+
return row ? row.cnt : 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Message queries
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get all messages for a session ordered by timestamp.
|
|
189
|
+
* @param {Object} db
|
|
190
|
+
* @param {string} sessionId
|
|
191
|
+
* @returns {Object[]}
|
|
192
|
+
*/
|
|
193
|
+
function getMessagesBySession(db, sessionId) {
|
|
194
|
+
return queryAll(
|
|
195
|
+
db,
|
|
196
|
+
'SELECT * FROM unified_message WHERE session_id = ? ORDER BY timestamp',
|
|
197
|
+
[sessionId]
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Insert multiple messages efficiently inside a transaction.
|
|
203
|
+
* @param {Object} db
|
|
204
|
+
* @param {Object[]} messages
|
|
205
|
+
*/
|
|
206
|
+
function bulkInsertMessages(db, messages) {
|
|
207
|
+
if (!messages.length) return;
|
|
208
|
+
|
|
209
|
+
const sql = `INSERT INTO unified_message (
|
|
210
|
+
id, session_id, source, role, timestamp,
|
|
211
|
+
tokens_input, tokens_output, tokens_reasoning,
|
|
212
|
+
cost_usd, content_length, is_error, model_id, text
|
|
213
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`;
|
|
214
|
+
|
|
215
|
+
db.run('BEGIN TRANSACTION');
|
|
216
|
+
try {
|
|
217
|
+
for (const m of messages) {
|
|
218
|
+
// content_length: prefer explicit value, otherwise derive from text
|
|
219
|
+
const len = m.content_length || (m.text ? m.text.length : 0);
|
|
220
|
+
db.run(sql, [
|
|
221
|
+
m.id,
|
|
222
|
+
m.session_id,
|
|
223
|
+
m.source,
|
|
224
|
+
m.role,
|
|
225
|
+
m.timestamp,
|
|
226
|
+
m.tokens_input || 0,
|
|
227
|
+
m.tokens_output || 0,
|
|
228
|
+
m.tokens_reasoning || 0,
|
|
229
|
+
m.cost_usd || 0,
|
|
230
|
+
len,
|
|
231
|
+
m.is_error || 0,
|
|
232
|
+
m.model_id || null,
|
|
233
|
+
m.text || null,
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
db.run('COMMIT');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
db.run('ROLLBACK');
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Part queries
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get all parts for a session.
|
|
249
|
+
* @param {Object} db
|
|
250
|
+
* @param {string} sessionId
|
|
251
|
+
* @returns {Object[]}
|
|
252
|
+
*/
|
|
253
|
+
function getPartsBySession(db, sessionId) {
|
|
254
|
+
return queryAll(
|
|
255
|
+
db,
|
|
256
|
+
'SELECT * FROM unified_part WHERE session_id = ? ORDER BY timestamp',
|
|
257
|
+
[sessionId]
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Insert multiple parts efficiently inside a transaction.
|
|
263
|
+
* @param {Object} db
|
|
264
|
+
* @param {Object[]} parts
|
|
265
|
+
*/
|
|
266
|
+
function bulkInsertParts(db, parts) {
|
|
267
|
+
if (!parts.length) return;
|
|
268
|
+
|
|
269
|
+
const sql = `INSERT INTO unified_part (
|
|
270
|
+
id, message_id, session_id, source, type, timestamp
|
|
271
|
+
) VALUES (?,?,?,?,?,?)`;
|
|
272
|
+
|
|
273
|
+
db.run('BEGIN TRANSACTION');
|
|
274
|
+
try {
|
|
275
|
+
for (const p of parts) {
|
|
276
|
+
db.run(sql, [
|
|
277
|
+
p.id,
|
|
278
|
+
p.message_id,
|
|
279
|
+
p.session_id,
|
|
280
|
+
p.source,
|
|
281
|
+
p.type,
|
|
282
|
+
p.timestamp,
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
db.run('COMMIT');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
db.run('ROLLBACK');
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Tool call queries
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get all tool calls for a session ordered by timestamp.
|
|
298
|
+
* @param {Object} db
|
|
299
|
+
* @param {string} sessionId
|
|
300
|
+
* @returns {Object[]}
|
|
301
|
+
*/
|
|
302
|
+
function getToolCallsBySession(db, sessionId) {
|
|
303
|
+
return queryAll(
|
|
304
|
+
db,
|
|
305
|
+
'SELECT * FROM unified_tool_call WHERE session_id = ? ORDER BY timestamp',
|
|
306
|
+
[sessionId]
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Insert multiple tool calls efficiently inside a transaction.
|
|
312
|
+
* @param {Object} db
|
|
313
|
+
* @param {Object[]} toolCalls
|
|
314
|
+
*/
|
|
315
|
+
function bulkInsertToolCalls(db, toolCalls) {
|
|
316
|
+
if (!toolCalls.length) return;
|
|
317
|
+
|
|
318
|
+
const sql = `INSERT INTO unified_tool_call (
|
|
319
|
+
id, part_id, session_id, source, tool_name, timestamp,
|
|
320
|
+
status, error_message, target_file
|
|
321
|
+
) VALUES (?,?,?,?,?,?,?,?,?)`;
|
|
322
|
+
|
|
323
|
+
db.run('BEGIN TRANSACTION');
|
|
324
|
+
try {
|
|
325
|
+
for (const tc of toolCalls) {
|
|
326
|
+
db.run(sql, [
|
|
327
|
+
tc.id,
|
|
328
|
+
tc.part_id,
|
|
329
|
+
tc.session_id,
|
|
330
|
+
tc.source,
|
|
331
|
+
tc.tool_name,
|
|
332
|
+
tc.timestamp,
|
|
333
|
+
tc.status || null,
|
|
334
|
+
tc.error_message || null,
|
|
335
|
+
tc.target_file || null,
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
db.run('COMMIT');
|
|
339
|
+
} catch (err) {
|
|
340
|
+
db.run('ROLLBACK');
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Analysis queries
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Insert or replace a session analysis row.
|
|
351
|
+
* @param {Object} db
|
|
352
|
+
* @param {Object} analysis
|
|
353
|
+
*/
|
|
354
|
+
function upsertSessionAnalysis(db, analysis) {
|
|
355
|
+
// v2 columns (H1..O1, difficulty, judge). The legacy v1 columns
|
|
356
|
+
// (score_a..score_e, sub_scores, highlight_dims) still exist on the
|
|
357
|
+
// table for back-compat with older boss.db files but are no longer
|
|
358
|
+
// written; they'll just remain at their historical values (NULL on
|
|
359
|
+
// freshly-analysed rows).
|
|
360
|
+
const sql = `INSERT OR REPLACE INTO session_analysis (
|
|
361
|
+
session_id, source, analyzed_at, status,
|
|
362
|
+
difficulty,
|
|
363
|
+
score_h1, level_h1,
|
|
364
|
+
score_h2, level_h2,
|
|
365
|
+
score_h3, level_h3,
|
|
366
|
+
score_e1, level_e1,
|
|
367
|
+
score_e2, level_e2,
|
|
368
|
+
score_o1, level_o1,
|
|
369
|
+
sub_scores_v2, llm_judge_v2, judge_source
|
|
370
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
|
|
371
|
+
|
|
372
|
+
db.run(sql, [
|
|
373
|
+
analysis.session_id,
|
|
374
|
+
analysis.source || null,
|
|
375
|
+
analysis.analyzed_at || null,
|
|
376
|
+
analysis.status || 'pending',
|
|
377
|
+
analysis.difficulty ?? null,
|
|
378
|
+
analysis.score_h1 ?? null, analysis.level_h1 ?? null,
|
|
379
|
+
analysis.score_h2 ?? null, analysis.level_h2 ?? null,
|
|
380
|
+
analysis.score_h3 ?? null, analysis.level_h3 ?? null,
|
|
381
|
+
analysis.score_e1 ?? null, analysis.level_e1 ?? null,
|
|
382
|
+
analysis.score_e2 ?? null, analysis.level_e2 ?? null,
|
|
383
|
+
analysis.score_o1 ?? null, analysis.level_o1 ?? null,
|
|
384
|
+
analysis.sub_scores_v2 || null,
|
|
385
|
+
analysis.llm_judge_v2 || null,
|
|
386
|
+
analysis.judge_source || null,
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get the analysis row for a session.
|
|
392
|
+
* @param {Object} db
|
|
393
|
+
* @param {string} sessionId
|
|
394
|
+
* @returns {Object|null}
|
|
395
|
+
*/
|
|
396
|
+
function getAnalysisBySession(db, sessionId) {
|
|
397
|
+
return queryOne(
|
|
398
|
+
db,
|
|
399
|
+
'SELECT * FROM session_analysis WHERE session_id = ?',
|
|
400
|
+
[sessionId]
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get sessions that need (re-)analysis for a given date.
|
|
406
|
+
* Picks sessions that have no analysis row, are 'pending', or whose
|
|
407
|
+
* data grew since the last pass (ended_at newer than analyzed_at —
|
|
408
|
+
* both ISO-8601 UTC, so string comparison is safe). The last clause
|
|
409
|
+
* also retries 'error' rows once the session has new data.
|
|
410
|
+
* @param {Object} db
|
|
411
|
+
* @param {string} date YYYY-MM-DD
|
|
412
|
+
* @returns {Object[]}
|
|
413
|
+
*/
|
|
414
|
+
function getUnanalyzedSessions(db, date) {
|
|
415
|
+
return queryAll(
|
|
416
|
+
db,
|
|
417
|
+
`SELECT s.* FROM unified_session s
|
|
418
|
+
LEFT JOIN session_analysis sa ON s.id = sa.session_id
|
|
419
|
+
WHERE s.date = ? AND (
|
|
420
|
+
sa.status IS NULL OR sa.status = 'pending'
|
|
421
|
+
OR (sa.analyzed_at IS NOT NULL AND s.ended_at > sa.analyzed_at)
|
|
422
|
+
)
|
|
423
|
+
ORDER BY s.started_at`,
|
|
424
|
+
[date]
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Insert or replace a daily summary row.
|
|
430
|
+
* @param {Object} db
|
|
431
|
+
* @param {Object} summary
|
|
432
|
+
*/
|
|
433
|
+
function upsertDailySummary(db, summary) {
|
|
434
|
+
const sql = `INSERT OR REPLACE INTO daily_summary (
|
|
435
|
+
id, date, source, session_count, message_count, tool_call_count,
|
|
436
|
+
tokens_input, tokens_output, tokens_reasoning,
|
|
437
|
+
tokens_cache_read, tokens_cache_write, cost_usd,
|
|
438
|
+
first_activity_at, last_activity_at, active_minutes,
|
|
439
|
+
peak_hour, error_count, revert_count, additions, deletions
|
|
440
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
|
|
441
|
+
|
|
442
|
+
db.run(sql, [
|
|
443
|
+
summary.id,
|
|
444
|
+
summary.date,
|
|
445
|
+
summary.source,
|
|
446
|
+
summary.session_count || 0,
|
|
447
|
+
summary.message_count || 0,
|
|
448
|
+
summary.tool_call_count || 0,
|
|
449
|
+
summary.tokens_input || 0,
|
|
450
|
+
summary.tokens_output || 0,
|
|
451
|
+
summary.tokens_reasoning || 0,
|
|
452
|
+
summary.tokens_cache_read || 0,
|
|
453
|
+
summary.tokens_cache_write || 0,
|
|
454
|
+
summary.cost_usd || 0,
|
|
455
|
+
summary.first_activity_at || null,
|
|
456
|
+
summary.last_activity_at || null,
|
|
457
|
+
summary.active_minutes || 0,
|
|
458
|
+
summary.peak_hour ?? null,
|
|
459
|
+
summary.error_count || 0,
|
|
460
|
+
summary.revert_count || 0,
|
|
461
|
+
summary.additions || 0,
|
|
462
|
+
summary.deletions || 0,
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get daily summary for a specific date and source.
|
|
468
|
+
* @param {Object} db
|
|
469
|
+
* @param {string} date
|
|
470
|
+
* @param {string} [source]
|
|
471
|
+
* @returns {Object|null}
|
|
472
|
+
*/
|
|
473
|
+
function getDailySummary(db, date, source) {
|
|
474
|
+
// Default to the 'all' rollup row to keep parity with getDailySummaries
|
|
475
|
+
// and prevent double-counting across sources.
|
|
476
|
+
const effectiveSource = source || 'all';
|
|
477
|
+
if (effectiveSource) {
|
|
478
|
+
return queryOne(
|
|
479
|
+
db,
|
|
480
|
+
'SELECT * FROM daily_summary WHERE date = ? AND source = ?',
|
|
481
|
+
[date, effectiveSource]
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return queryOne(
|
|
485
|
+
db,
|
|
486
|
+
'SELECT * FROM daily_summary WHERE date = ?',
|
|
487
|
+
[date]
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get daily summaries in a date range.
|
|
493
|
+
* @param {Object} db
|
|
494
|
+
* @param {string} fromDate
|
|
495
|
+
* @param {string} toDate
|
|
496
|
+
* @param {string} [source]
|
|
497
|
+
* @returns {Object[]}
|
|
498
|
+
*/
|
|
499
|
+
function getDailySummaries(db, fromDate, toDate, source) {
|
|
500
|
+
// When no source is specified, return the cross-source rollup ('all') to
|
|
501
|
+
// avoid double-counting (one row per source PLUS one 'all' row are written
|
|
502
|
+
// per date by the daily aggregator).
|
|
503
|
+
const effectiveSource = source || 'all';
|
|
504
|
+
return queryAll(
|
|
505
|
+
db,
|
|
506
|
+
'SELECT * FROM daily_summary WHERE date >= ? AND date <= ? AND source = ? ORDER BY date',
|
|
507
|
+
[fromDate, toDate, effectiveSource]
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Hourly activity queries
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Insert or replace an hourly activity row.
|
|
517
|
+
* @param {Object} db
|
|
518
|
+
* @param {Object} activity
|
|
519
|
+
*/
|
|
520
|
+
function upsertHourlyActivity(db, activity) {
|
|
521
|
+
const sql = `INSERT OR REPLACE INTO hourly_activity (
|
|
522
|
+
date, hour, source,
|
|
523
|
+
message_count, session_count, error_count, tool_call_count
|
|
524
|
+
) VALUES (?,?,?,?,?,?,?)`;
|
|
525
|
+
|
|
526
|
+
db.run(sql, [
|
|
527
|
+
activity.date,
|
|
528
|
+
activity.hour,
|
|
529
|
+
activity.source,
|
|
530
|
+
activity.message_count || 0,
|
|
531
|
+
activity.session_count || 0,
|
|
532
|
+
activity.error_count || 0,
|
|
533
|
+
activity.tool_call_count || 0,
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Get hourly activity rows for a date, optionally filtered by source.
|
|
539
|
+
* @param {Object} db
|
|
540
|
+
* @param {string} date
|
|
541
|
+
* @param {string} [source]
|
|
542
|
+
* @returns {Object[]}
|
|
543
|
+
*/
|
|
544
|
+
function getHourlyActivity(db, date, source) {
|
|
545
|
+
if (source) {
|
|
546
|
+
return queryAll(
|
|
547
|
+
db,
|
|
548
|
+
'SELECT * FROM hourly_activity WHERE date = ? AND source = ? ORDER BY hour',
|
|
549
|
+
[date, source]
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return queryAll(
|
|
553
|
+
db,
|
|
554
|
+
'SELECT * FROM hourly_activity WHERE date = ? ORDER BY hour',
|
|
555
|
+
[date]
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Settings queries
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get a single setting value by key.
|
|
565
|
+
* @param {Object} db
|
|
566
|
+
* @param {string} key
|
|
567
|
+
* @returns {string|null}
|
|
568
|
+
*/
|
|
569
|
+
function getSetting(db, key) {
|
|
570
|
+
const row = queryOne(db, 'SELECT value FROM user_settings WHERE key = ?', [key]);
|
|
571
|
+
return row ? row.value : null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Set a setting value (insert or update).
|
|
576
|
+
* @param {Object} db
|
|
577
|
+
* @param {string} key
|
|
578
|
+
* @param {string} value
|
|
579
|
+
*/
|
|
580
|
+
function setSetting(db, key, value) {
|
|
581
|
+
db.run(
|
|
582
|
+
'INSERT OR REPLACE INTO user_settings (key, value) VALUES (?, ?)',
|
|
583
|
+
[key, value]
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Get all settings as a plain object { key: value }.
|
|
589
|
+
* @param {Object} db
|
|
590
|
+
* @returns {Object}
|
|
591
|
+
*/
|
|
592
|
+
function getAllSettings(db) {
|
|
593
|
+
const rows = queryAll(db, 'SELECT key, value FROM user_settings');
|
|
594
|
+
const settings = {};
|
|
595
|
+
for (const row of rows) {
|
|
596
|
+
settings[row.key] = row.value;
|
|
597
|
+
}
|
|
598
|
+
return settings;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// ETL state queries
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get ETL state for a source.
|
|
607
|
+
* @param {Object} db
|
|
608
|
+
* @param {string} source
|
|
609
|
+
* @returns {Object|null}
|
|
610
|
+
*/
|
|
611
|
+
function getEtlState(db, source) {
|
|
612
|
+
return queryOne(db, 'SELECT * FROM etl_state WHERE source = ?', [source]);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Update ETL state for a source (upsert).
|
|
617
|
+
* @param {Object} db
|
|
618
|
+
* @param {string} source
|
|
619
|
+
* @param {Object} data
|
|
620
|
+
*/
|
|
621
|
+
function updateEtlState(db, source, data) {
|
|
622
|
+
const sql = `INSERT OR REPLACE INTO etl_state (
|
|
623
|
+
source, last_sync_at, last_session_id, last_session_time, status
|
|
624
|
+
) VALUES (?,?,?,?,?)`;
|
|
625
|
+
|
|
626
|
+
db.run(sql, [
|
|
627
|
+
source,
|
|
628
|
+
data.last_sync_at || null,
|
|
629
|
+
data.last_session_id || null,
|
|
630
|
+
data.last_session_time || null,
|
|
631
|
+
data.status || 'idle',
|
|
632
|
+
]);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// Analysis state queries
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get analysis state (singleton row, id=1).
|
|
641
|
+
* @param {Object} db
|
|
642
|
+
* @returns {Object|null}
|
|
643
|
+
*/
|
|
644
|
+
function getAnalysisState(db) {
|
|
645
|
+
return queryOne(db, 'SELECT * FROM analysis_state WHERE id = 1');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Update the analysis state (singleton row, id=1).
|
|
650
|
+
* @param {Object} db
|
|
651
|
+
* @param {Object} data
|
|
652
|
+
*/
|
|
653
|
+
function updateAnalysisState(db, data) {
|
|
654
|
+
const sql = `INSERT OR REPLACE INTO analysis_state (
|
|
655
|
+
id, status, current_date, analyzed_count, total_count, last_analyzed_at
|
|
656
|
+
) VALUES (1,?,?,?,?,?)`;
|
|
657
|
+
|
|
658
|
+
db.run(sql, [
|
|
659
|
+
data.status || 'idle',
|
|
660
|
+
data.current_date || null,
|
|
661
|
+
data.analyzed_count || 0,
|
|
662
|
+
data.total_count || 0,
|
|
663
|
+
data.last_analyzed_at || null,
|
|
664
|
+
]);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
// Tool config queries
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Get configuration for a specific tool.
|
|
673
|
+
* @param {Object} db
|
|
674
|
+
* @param {string} tool
|
|
675
|
+
* @returns {Object|null}
|
|
676
|
+
*/
|
|
677
|
+
function getToolConfig(db, tool) {
|
|
678
|
+
return queryOne(db, 'SELECT * FROM tool_config WHERE tool = ?', [tool]);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get all tool configurations.
|
|
683
|
+
* @param {Object} db
|
|
684
|
+
* @returns {Object[]}
|
|
685
|
+
*/
|
|
686
|
+
function getAllToolConfigs(db) {
|
|
687
|
+
return queryAll(db, 'SELECT * FROM tool_config ORDER BY tool');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Insert or replace a tool configuration.
|
|
692
|
+
* @param {Object} db
|
|
693
|
+
* @param {Object} config
|
|
694
|
+
*/
|
|
695
|
+
function upsertToolConfig(db, config) {
|
|
696
|
+
const sql = `INSERT OR REPLACE INTO tool_config (
|
|
697
|
+
tool, enabled, data_path, status, label
|
|
698
|
+
) VALUES (?,?,?,?,?)`;
|
|
699
|
+
|
|
700
|
+
db.run(sql, [
|
|
701
|
+
config.tool,
|
|
702
|
+
config.enabled ?? 1,
|
|
703
|
+
config.data_path || null,
|
|
704
|
+
config.status || 'unknown',
|
|
705
|
+
config.label,
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// Overview (home page) — ETL-direct aggregations
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
//
|
|
713
|
+
// All four helpers below read only from unified_session and never touch the
|
|
714
|
+
// analysis-layer tables. This guarantees the home page renders immediately
|
|
715
|
+
// after ETL, without waiting for the LLM-driven scoring job to catch up.
|
|
716
|
+
//
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Earliest session date present in unified_session, or null if empty.
|
|
720
|
+
* Used by the overview API to resolve the "all-time" date range without
|
|
721
|
+
* forking every helper to accept an open-ended bound.
|
|
722
|
+
*
|
|
723
|
+
* @param {Object} db
|
|
724
|
+
* @returns {string|null} YYYY-MM-DD or null
|
|
725
|
+
*/
|
|
726
|
+
function getEarliestSessionDate(db) {
|
|
727
|
+
const r = queryOne(db, 'SELECT MIN(date) AS d FROM unified_session');
|
|
728
|
+
return r && r.d ? r.d : null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Single-day snapshot aggregated across all sources.
|
|
733
|
+
* Returns a zero-filled object (never null) so the UI can render safely.
|
|
734
|
+
*
|
|
735
|
+
* @param {Object} db
|
|
736
|
+
* @param {string} date YYYY-MM-DD
|
|
737
|
+
* @returns {Object}
|
|
738
|
+
*/
|
|
739
|
+
function getOverviewSnapshot(db, date) {
|
|
740
|
+
const row = queryOne(
|
|
741
|
+
db,
|
|
742
|
+
`SELECT
|
|
743
|
+
COUNT(*) AS sessions,
|
|
744
|
+
COALESCE(SUM(active_minutes), 0) AS active_minutes,
|
|
745
|
+
COALESCE(SUM(cost_usd), 0) AS cost,
|
|
746
|
+
COALESCE(SUM(tokens_input), 0) AS tokens_input,
|
|
747
|
+
COALESCE(SUM(tokens_output), 0) AS tokens_output,
|
|
748
|
+
COALESCE(SUM(message_count), 0) AS messages,
|
|
749
|
+
COALESCE(SUM(error_count), 0) AS errors,
|
|
750
|
+
COALESCE(SUM(reverted), 0) AS reverted
|
|
751
|
+
FROM unified_session
|
|
752
|
+
WHERE date = ?`,
|
|
753
|
+
[date]
|
|
754
|
+
) || {};
|
|
755
|
+
|
|
756
|
+
const tokensInput = row.tokens_input || 0;
|
|
757
|
+
const tokensOutput = row.tokens_output || 0;
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
date,
|
|
761
|
+
sessions: row.sessions || 0,
|
|
762
|
+
activeMinutes: row.active_minutes || 0,
|
|
763
|
+
cost: Math.round((row.cost || 0) * 10000) / 10000,
|
|
764
|
+
tokensInput,
|
|
765
|
+
tokensOutput,
|
|
766
|
+
tokensTotal: tokensInput + tokensOutput,
|
|
767
|
+
messages: row.messages || 0,
|
|
768
|
+
errors: row.errors || 0,
|
|
769
|
+
reverted: row.reverted || 0,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Daily trend rows for the home page, broken out by source so the UI can
|
|
775
|
+
* render two overlaid lines (opencode vs claude-code).
|
|
776
|
+
*
|
|
777
|
+
* @param {Object} db
|
|
778
|
+
* @param {string} fromDate YYYY-MM-DD (inclusive)
|
|
779
|
+
* @param {string} toDate YYYY-MM-DD (inclusive)
|
|
780
|
+
* @returns {Object[]}
|
|
781
|
+
*/
|
|
782
|
+
function getOverviewTrend(db, fromDate, toDate) {
|
|
783
|
+
return queryAll(
|
|
784
|
+
db,
|
|
785
|
+
`SELECT
|
|
786
|
+
date,
|
|
787
|
+
source,
|
|
788
|
+
COUNT(*) AS sessions,
|
|
789
|
+
COALESCE(SUM(cost_usd), 0) AS cost,
|
|
790
|
+
COALESCE(SUM(active_minutes), 0) AS active_minutes
|
|
791
|
+
FROM unified_session
|
|
792
|
+
WHERE date BETWEEN ? AND ?
|
|
793
|
+
GROUP BY date, source
|
|
794
|
+
ORDER BY date ASC, source ASC`,
|
|
795
|
+
[fromDate, toDate]
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Top projects within a date range, ranked by total cost (USD).
|
|
801
|
+
* Excludes rows where project is NULL / empty.
|
|
802
|
+
*
|
|
803
|
+
* @param {Object} db
|
|
804
|
+
* @param {string} fromDate
|
|
805
|
+
* @param {string} toDate
|
|
806
|
+
* @param {number} [limit=8]
|
|
807
|
+
* @returns {Object[]}
|
|
808
|
+
*/
|
|
809
|
+
function getOverviewTopProjects(db, fromDate, toDate, limit = 8) {
|
|
810
|
+
return queryAll(
|
|
811
|
+
db,
|
|
812
|
+
`SELECT
|
|
813
|
+
project,
|
|
814
|
+
COUNT(*) AS sessions,
|
|
815
|
+
COALESCE(SUM(cost_usd), 0) AS cost,
|
|
816
|
+
COALESCE(SUM(active_minutes), 0) AS active_minutes,
|
|
817
|
+
COALESCE(SUM(summary_additions), 0) AS additions,
|
|
818
|
+
COALESCE(SUM(summary_deletions), 0) AS deletions,
|
|
819
|
+
COALESCE(SUM(summary_files), 0) AS files
|
|
820
|
+
FROM unified_session
|
|
821
|
+
WHERE date BETWEEN ? AND ?
|
|
822
|
+
AND project IS NOT NULL
|
|
823
|
+
AND project <> ''
|
|
824
|
+
GROUP BY project
|
|
825
|
+
ORDER BY cost DESC, sessions DESC
|
|
826
|
+
LIMIT ?`,
|
|
827
|
+
[fromDate, toDate, limit]
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Per-day cache effectiveness (hit-rate) for a date window.
|
|
833
|
+
* Hit rate = cache_read / (cache_read + cache_write + tokens_input).
|
|
834
|
+
* See the implementation below for why cache_write is in the denominator.
|
|
835
|
+
* Raw numerator/denominator are returned so the UI can change the formula
|
|
836
|
+
* later without another query.
|
|
837
|
+
*
|
|
838
|
+
* @param {Object} db
|
|
839
|
+
* @param {string} fromDate
|
|
840
|
+
* @param {string} toDate
|
|
841
|
+
* @returns {Object[]}
|
|
842
|
+
*/
|
|
843
|
+
function getOverviewCacheRate(db, fromDate, toDate) {
|
|
844
|
+
const rows = queryAll(
|
|
845
|
+
db,
|
|
846
|
+
`SELECT
|
|
847
|
+
date,
|
|
848
|
+
COALESCE(SUM(tokens_input), 0) AS tokens_input,
|
|
849
|
+
COALESCE(SUM(tokens_cache_read), 0) AS cache_read,
|
|
850
|
+
COALESCE(SUM(tokens_cache_write), 0) AS cache_write
|
|
851
|
+
FROM unified_session
|
|
852
|
+
WHERE date BETWEEN ? AND ?
|
|
853
|
+
GROUP BY date
|
|
854
|
+
ORDER BY date ASC`,
|
|
855
|
+
[fromDate, toDate]
|
|
856
|
+
);
|
|
857
|
+
return rows.map((r) => {
|
|
858
|
+
const input = r.tokens_input || 0;
|
|
859
|
+
const cr = r.cache_read || 0;
|
|
860
|
+
const cw = r.cache_write || 0;
|
|
861
|
+
// Hit rate = reused cached input / total input tokens the model saw.
|
|
862
|
+
// Including cache_write in the denominator matters: those are tokens
|
|
863
|
+
// that *paid full price* this turn (writing them to the cache); only
|
|
864
|
+
// future turns benefit. Excluding cache_write would always inflate
|
|
865
|
+
// the rate toward ~100% in long sessions and obscure the moment a
|
|
866
|
+
// huge new context block was loaded.
|
|
867
|
+
const denom = cr + cw + input;
|
|
868
|
+
return {
|
|
869
|
+
date: r.date,
|
|
870
|
+
input,
|
|
871
|
+
cacheRead: cr,
|
|
872
|
+
cacheWrite: cw,
|
|
873
|
+
hitRate: denom > 0 ? Math.round((cr / denom) * 1000) / 10 : 0, // %
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Per-day error rate for a date window.
|
|
880
|
+
* rate = SUM(error_count) / NULLIF(SUM(message_count), 0)
|
|
881
|
+
*
|
|
882
|
+
* @param {Object} db
|
|
883
|
+
* @param {string} fromDate
|
|
884
|
+
* @param {string} toDate
|
|
885
|
+
* @returns {Object[]}
|
|
886
|
+
*/
|
|
887
|
+
function getOverviewErrorRate(db, fromDate, toDate) {
|
|
888
|
+
const rows = queryAll(
|
|
889
|
+
db,
|
|
890
|
+
`SELECT
|
|
891
|
+
date,
|
|
892
|
+
COALESCE(SUM(error_count), 0) AS errors,
|
|
893
|
+
COALESCE(SUM(message_count), 0) AS messages,
|
|
894
|
+
COALESCE(SUM(tool_call_count),0) AS tool_calls
|
|
895
|
+
FROM unified_session
|
|
896
|
+
WHERE date BETWEEN ? AND ?
|
|
897
|
+
GROUP BY date
|
|
898
|
+
ORDER BY date ASC`,
|
|
899
|
+
[fromDate, toDate]
|
|
900
|
+
);
|
|
901
|
+
return rows.map((r) => ({
|
|
902
|
+
date: r.date,
|
|
903
|
+
errors: r.errors || 0,
|
|
904
|
+
messages: r.messages || 0,
|
|
905
|
+
toolCalls: r.tool_calls || 0,
|
|
906
|
+
rate: r.messages > 0
|
|
907
|
+
? Math.round((r.errors / r.messages) * 10000) / 100 // % with 2 decimals
|
|
908
|
+
: 0,
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Tool usage Top-N within a date window, with error-rate per tool.
|
|
914
|
+
* Joins unified_tool_call -> unified_session to apply the date filter.
|
|
915
|
+
*
|
|
916
|
+
* @param {Object} db
|
|
917
|
+
* @param {string} fromDate
|
|
918
|
+
* @param {string} toDate
|
|
919
|
+
* @param {number} [limit=10]
|
|
920
|
+
* @returns {Object[]}
|
|
921
|
+
*/
|
|
922
|
+
function getOverviewTopTools(db, fromDate, toDate, limit = 10) {
|
|
923
|
+
const rows = queryAll(
|
|
924
|
+
db,
|
|
925
|
+
`SELECT
|
|
926
|
+
tc.tool_name AS tool,
|
|
927
|
+
COUNT(*) AS calls,
|
|
928
|
+
SUM(CASE WHEN tc.status = 'error' THEN 1 ELSE 0 END) AS errors,
|
|
929
|
+
COUNT(DISTINCT tc.session_id) AS sessions
|
|
930
|
+
FROM unified_tool_call tc
|
|
931
|
+
JOIN unified_session s ON s.id = tc.session_id
|
|
932
|
+
WHERE s.date BETWEEN ? AND ?
|
|
933
|
+
AND tc.tool_name IS NOT NULL
|
|
934
|
+
AND tc.tool_name <> ''
|
|
935
|
+
GROUP BY tc.tool_name
|
|
936
|
+
ORDER BY calls DESC
|
|
937
|
+
LIMIT ?`,
|
|
938
|
+
[fromDate, toDate, limit]
|
|
939
|
+
);
|
|
940
|
+
return rows.map((r) => ({
|
|
941
|
+
tool: r.tool,
|
|
942
|
+
calls: r.calls || 0,
|
|
943
|
+
errors: r.errors || 0,
|
|
944
|
+
sessions: r.sessions || 0,
|
|
945
|
+
errorRate: r.calls > 0
|
|
946
|
+
? Math.round((r.errors / r.calls) * 1000) / 10 // % with 1 decimal
|
|
947
|
+
: 0,
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Most recent sessions across all sources / dates.
|
|
953
|
+
*
|
|
954
|
+
* Excludes subagents (parent_session_id IS NOT NULL) by default so the
|
|
955
|
+
* "最近 10 个会话" panel only shows top-level work; aggregate stats on
|
|
956
|
+
* the same page still include them via their own queries.
|
|
957
|
+
*
|
|
958
|
+
* @param {Object} db
|
|
959
|
+
* @param {number} [limit=10]
|
|
960
|
+
* @returns {Object[]}
|
|
961
|
+
*/
|
|
962
|
+
function getOverviewRecentSessions(db, limit = 10) {
|
|
963
|
+
return queryAll(
|
|
964
|
+
db,
|
|
965
|
+
`SELECT
|
|
966
|
+
id,
|
|
967
|
+
source,
|
|
968
|
+
started_at,
|
|
969
|
+
project,
|
|
970
|
+
title,
|
|
971
|
+
model,
|
|
972
|
+
message_count,
|
|
973
|
+
cost_usd,
|
|
974
|
+
error_count,
|
|
975
|
+
reverted
|
|
976
|
+
FROM unified_session
|
|
977
|
+
WHERE parent_session_id IS NULL
|
|
978
|
+
ORDER BY started_at DESC
|
|
979
|
+
LIMIT ?`,
|
|
980
|
+
[limit]
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ---------------------------------------------------------------------------
|
|
985
|
+
// Exports
|
|
986
|
+
// ---------------------------------------------------------------------------
|
|
987
|
+
|
|
988
|
+
module.exports = {
|
|
989
|
+
// Internal helpers (exported for testing / advanced use)
|
|
990
|
+
queryAll,
|
|
991
|
+
queryOne,
|
|
992
|
+
|
|
993
|
+
// Session
|
|
994
|
+
upsertSession,
|
|
995
|
+
getSessionsByDate,
|
|
996
|
+
getSessionById,
|
|
997
|
+
getSessionsByDateRange,
|
|
998
|
+
countSessionsByDateRange,
|
|
999
|
+
|
|
1000
|
+
// Message
|
|
1001
|
+
getMessagesBySession,
|
|
1002
|
+
bulkInsertMessages,
|
|
1003
|
+
|
|
1004
|
+
// Part
|
|
1005
|
+
getPartsBySession,
|
|
1006
|
+
bulkInsertParts,
|
|
1007
|
+
|
|
1008
|
+
// Tool call
|
|
1009
|
+
getToolCallsBySession,
|
|
1010
|
+
bulkInsertToolCalls,
|
|
1011
|
+
|
|
1012
|
+
// Analysis
|
|
1013
|
+
upsertSessionAnalysis,
|
|
1014
|
+
getAnalysisBySession,
|
|
1015
|
+
getUnanalyzedSessions,
|
|
1016
|
+
upsertDailySummary,
|
|
1017
|
+
getDailySummary,
|
|
1018
|
+
getDailySummaries,
|
|
1019
|
+
|
|
1020
|
+
// Hourly activity
|
|
1021
|
+
upsertHourlyActivity,
|
|
1022
|
+
getHourlyActivity,
|
|
1023
|
+
|
|
1024
|
+
// Settings
|
|
1025
|
+
getSetting,
|
|
1026
|
+
setSetting,
|
|
1027
|
+
getAllSettings,
|
|
1028
|
+
|
|
1029
|
+
// ETL state
|
|
1030
|
+
getEtlState,
|
|
1031
|
+
updateEtlState,
|
|
1032
|
+
|
|
1033
|
+
// Analysis state
|
|
1034
|
+
getAnalysisState,
|
|
1035
|
+
updateAnalysisState,
|
|
1036
|
+
|
|
1037
|
+
// Tool config
|
|
1038
|
+
getToolConfig,
|
|
1039
|
+
getAllToolConfigs,
|
|
1040
|
+
upsertToolConfig,
|
|
1041
|
+
|
|
1042
|
+
// Overview (home page) — ETL-direct
|
|
1043
|
+
getOverviewSnapshot,
|
|
1044
|
+
getOverviewTrend,
|
|
1045
|
+
getOverviewTopProjects,
|
|
1046
|
+
getOverviewRecentSessions,
|
|
1047
|
+
getOverviewCacheRate,
|
|
1048
|
+
getOverviewErrorRate,
|
|
1049
|
+
getOverviewTopTools,
|
|
1050
|
+
getEarliestSessionDate,
|
|
1051
|
+
};
|