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,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema for Agent Boss (boss.db)
|
|
3
|
+
* All tables as defined in design doc §5
|
|
4
|
+
*
|
|
5
|
+
* Uses sql.js (SQLite compiled to WebAssembly) for zero-native-dependency
|
|
6
|
+
* portability. The database lives in-memory with explicit file persistence
|
|
7
|
+
* managed by connection.js.
|
|
8
|
+
*
|
|
9
|
+
* @author Felix
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the default path to boss.db.
|
|
17
|
+
*
|
|
18
|
+
* @returns {string} Absolute path to ~/.agent-boss/boss.db
|
|
19
|
+
*/
|
|
20
|
+
function getDbPath() {
|
|
21
|
+
const dir = path.join(os.homedir(), '.agent-boss');
|
|
22
|
+
return path.join(dir, 'boss.db');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialise all tables, indexes, and default seed data.
|
|
27
|
+
*
|
|
28
|
+
* sql.js notes:
|
|
29
|
+
* - db.run(sql, params) executes a single statement (with optional bindings).
|
|
30
|
+
* - db.exec(sql) executes one-or-more statements (no bindings).
|
|
31
|
+
* - No WAL mode — sql.js operates in-memory; persistence is handled
|
|
32
|
+
* externally via db.export() + fs.writeFileSync().
|
|
33
|
+
*
|
|
34
|
+
* @param {import('sql.js').Database} db An sql.js Database instance
|
|
35
|
+
*/
|
|
36
|
+
function initDatabase(db) {
|
|
37
|
+
// Enable foreign keys (WAL & busy_timeout are N/A for in-memory sql.js)
|
|
38
|
+
db.run('PRAGMA foreign_keys = ON;');
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------
|
|
41
|
+
// §5.1 Unified intermediate layer (ETL writes)
|
|
42
|
+
// ------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS unified_session (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
source TEXT NOT NULL,
|
|
48
|
+
date TEXT NOT NULL,
|
|
49
|
+
started_at TEXT NOT NULL,
|
|
50
|
+
ended_at TEXT,
|
|
51
|
+
duration_minutes INTEGER,
|
|
52
|
+
active_minutes INTEGER,
|
|
53
|
+
message_count INTEGER DEFAULT 0,
|
|
54
|
+
tokens_input INTEGER DEFAULT 0,
|
|
55
|
+
tokens_output INTEGER DEFAULT 0,
|
|
56
|
+
tokens_reasoning INTEGER DEFAULT 0,
|
|
57
|
+
tokens_cache_read INTEGER DEFAULT 0,
|
|
58
|
+
tokens_cache_write INTEGER DEFAULT 0,
|
|
59
|
+
cost_usd REAL DEFAULT 0,
|
|
60
|
+
project TEXT,
|
|
61
|
+
title TEXT,
|
|
62
|
+
model TEXT,
|
|
63
|
+
error_count INTEGER DEFAULT 0,
|
|
64
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
65
|
+
summary_additions INTEGER DEFAULT 0,
|
|
66
|
+
summary_deletions INTEGER DEFAULT 0,
|
|
67
|
+
summary_files INTEGER DEFAULT 0,
|
|
68
|
+
reverted INTEGER DEFAULT 0,
|
|
69
|
+
time_compacting REAL DEFAULT 0
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_unified_session_date
|
|
73
|
+
ON unified_session(date);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_unified_session_source_date
|
|
75
|
+
ON unified_session(source, date);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_unified_session_project
|
|
77
|
+
ON unified_session(project);
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
db.exec(`
|
|
81
|
+
CREATE TABLE IF NOT EXISTS unified_message (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
session_id TEXT NOT NULL,
|
|
84
|
+
source TEXT NOT NULL,
|
|
85
|
+
role TEXT NOT NULL,
|
|
86
|
+
timestamp TEXT NOT NULL,
|
|
87
|
+
tokens_input INTEGER DEFAULT 0,
|
|
88
|
+
tokens_output INTEGER DEFAULT 0,
|
|
89
|
+
tokens_reasoning INTEGER DEFAULT 0,
|
|
90
|
+
cost_usd REAL DEFAULT 0,
|
|
91
|
+
content_length INTEGER DEFAULT 0,
|
|
92
|
+
is_error INTEGER DEFAULT 0,
|
|
93
|
+
model_id TEXT
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_unified_message_session
|
|
97
|
+
ON unified_message(session_id, timestamp);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_unified_message_source
|
|
99
|
+
ON unified_message(source, timestamp);
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
db.exec(`
|
|
103
|
+
CREATE TABLE IF NOT EXISTS unified_part (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
message_id TEXT NOT NULL,
|
|
106
|
+
session_id TEXT NOT NULL,
|
|
107
|
+
source TEXT NOT NULL,
|
|
108
|
+
type TEXT NOT NULL,
|
|
109
|
+
timestamp TEXT NOT NULL
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_unified_part_session
|
|
113
|
+
ON unified_part(session_id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_unified_part_message
|
|
115
|
+
ON unified_part(message_id);
|
|
116
|
+
`);
|
|
117
|
+
|
|
118
|
+
db.exec(`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS unified_tool_call (
|
|
120
|
+
id TEXT PRIMARY KEY,
|
|
121
|
+
part_id TEXT NOT NULL,
|
|
122
|
+
session_id TEXT NOT NULL,
|
|
123
|
+
source TEXT NOT NULL,
|
|
124
|
+
tool_name TEXT NOT NULL,
|
|
125
|
+
timestamp TEXT NOT NULL,
|
|
126
|
+
status TEXT,
|
|
127
|
+
error_message TEXT,
|
|
128
|
+
target_file TEXT
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_unified_tool_call_session
|
|
132
|
+
ON unified_tool_call(session_id, timestamp);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_unified_tool_call_tool
|
|
134
|
+
ON unified_tool_call(tool_name);
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
// ------------------------------------------------------------------
|
|
138
|
+
// §5.2 Analysis results layer (analysis job writes)
|
|
139
|
+
// ------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
db.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS session_analysis (
|
|
143
|
+
session_id TEXT PRIMARY KEY,
|
|
144
|
+
source TEXT,
|
|
145
|
+
analyzed_at TEXT,
|
|
146
|
+
score_a REAL,
|
|
147
|
+
score_b REAL,
|
|
148
|
+
score_c REAL,
|
|
149
|
+
score_d REAL,
|
|
150
|
+
score_e REAL,
|
|
151
|
+
sub_scores TEXT,
|
|
152
|
+
highlight_dims TEXT,
|
|
153
|
+
status TEXT DEFAULT 'pending'
|
|
154
|
+
);
|
|
155
|
+
`);
|
|
156
|
+
|
|
157
|
+
db.exec(`
|
|
158
|
+
CREATE TABLE IF NOT EXISTS daily_summary (
|
|
159
|
+
id TEXT PRIMARY KEY,
|
|
160
|
+
date TEXT NOT NULL,
|
|
161
|
+
source TEXT NOT NULL,
|
|
162
|
+
session_count INTEGER DEFAULT 0,
|
|
163
|
+
message_count INTEGER DEFAULT 0,
|
|
164
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
165
|
+
tokens_input INTEGER DEFAULT 0,
|
|
166
|
+
tokens_output INTEGER DEFAULT 0,
|
|
167
|
+
tokens_reasoning INTEGER DEFAULT 0,
|
|
168
|
+
tokens_cache_read INTEGER DEFAULT 0,
|
|
169
|
+
tokens_cache_write INTEGER DEFAULT 0,
|
|
170
|
+
cost_usd REAL DEFAULT 0,
|
|
171
|
+
first_activity_at TEXT,
|
|
172
|
+
last_activity_at TEXT,
|
|
173
|
+
active_minutes INTEGER DEFAULT 0,
|
|
174
|
+
peak_hour INTEGER,
|
|
175
|
+
error_count INTEGER DEFAULT 0,
|
|
176
|
+
revert_count INTEGER DEFAULT 0,
|
|
177
|
+
additions INTEGER DEFAULT 0,
|
|
178
|
+
deletions INTEGER DEFAULT 0
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_daily_summary_date
|
|
182
|
+
ON daily_summary(date);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_daily_summary_source_date
|
|
184
|
+
ON daily_summary(source, date);
|
|
185
|
+
`);
|
|
186
|
+
|
|
187
|
+
db.exec(`
|
|
188
|
+
CREATE TABLE IF NOT EXISTS hourly_activity (
|
|
189
|
+
date TEXT NOT NULL,
|
|
190
|
+
hour INTEGER NOT NULL,
|
|
191
|
+
source TEXT NOT NULL,
|
|
192
|
+
message_count INTEGER DEFAULT 0,
|
|
193
|
+
session_count INTEGER DEFAULT 0,
|
|
194
|
+
error_count INTEGER DEFAULT 0,
|
|
195
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
196
|
+
PRIMARY KEY (date, hour, source)
|
|
197
|
+
);
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
db.exec(`
|
|
201
|
+
CREATE TABLE IF NOT EXISTS dimension_score (
|
|
202
|
+
id TEXT PRIMARY KEY,
|
|
203
|
+
date TEXT,
|
|
204
|
+
period TEXT,
|
|
205
|
+
source TEXT,
|
|
206
|
+
score_a REAL,
|
|
207
|
+
score_b REAL,
|
|
208
|
+
score_c REAL,
|
|
209
|
+
score_d REAL,
|
|
210
|
+
score_e REAL,
|
|
211
|
+
session_count INTEGER
|
|
212
|
+
);
|
|
213
|
+
`);
|
|
214
|
+
|
|
215
|
+
db.exec(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS insight (
|
|
217
|
+
id TEXT PRIMARY KEY,
|
|
218
|
+
date TEXT,
|
|
219
|
+
period TEXT,
|
|
220
|
+
dimension TEXT,
|
|
221
|
+
type TEXT,
|
|
222
|
+
text TEXT,
|
|
223
|
+
severity TEXT,
|
|
224
|
+
evidence TEXT
|
|
225
|
+
);
|
|
226
|
+
`);
|
|
227
|
+
|
|
228
|
+
db.exec(`
|
|
229
|
+
CREATE TABLE IF NOT EXISTS collab_bill (
|
|
230
|
+
id TEXT PRIMARY KEY,
|
|
231
|
+
date TEXT NOT NULL,
|
|
232
|
+
period TEXT NOT NULL,
|
|
233
|
+
source TEXT,
|
|
234
|
+
rework_minutes REAL DEFAULT 0,
|
|
235
|
+
abandon_minutes REAL DEFAULT 0,
|
|
236
|
+
idle_output_minutes REAL DEFAULT 0,
|
|
237
|
+
model_mismatch_cost REAL DEFAULT 0,
|
|
238
|
+
model_mismatch_count INTEGER DEFAULT 0,
|
|
239
|
+
sunk_cost_usd REAL DEFAULT 0,
|
|
240
|
+
sunk_cost_minutes REAL DEFAULT 0,
|
|
241
|
+
sunk_session_count INTEGER DEFAULT 0,
|
|
242
|
+
hourly_rate REAL
|
|
243
|
+
);
|
|
244
|
+
`);
|
|
245
|
+
|
|
246
|
+
// ------------------------------------------------------------------
|
|
247
|
+
// §5.3 Configuration & state layer
|
|
248
|
+
// ------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
db.exec(`
|
|
251
|
+
CREATE TABLE IF NOT EXISTS tool_config (
|
|
252
|
+
tool TEXT PRIMARY KEY,
|
|
253
|
+
enabled INTEGER DEFAULT 1,
|
|
254
|
+
data_path TEXT,
|
|
255
|
+
status TEXT DEFAULT 'unknown',
|
|
256
|
+
label TEXT NOT NULL
|
|
257
|
+
);
|
|
258
|
+
`);
|
|
259
|
+
|
|
260
|
+
db.exec(`
|
|
261
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
262
|
+
key TEXT PRIMARY KEY,
|
|
263
|
+
value TEXT
|
|
264
|
+
);
|
|
265
|
+
`);
|
|
266
|
+
|
|
267
|
+
db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS etl_state (
|
|
269
|
+
source TEXT PRIMARY KEY,
|
|
270
|
+
last_sync_at TEXT,
|
|
271
|
+
last_session_id TEXT,
|
|
272
|
+
last_session_time TEXT,
|
|
273
|
+
status TEXT DEFAULT 'idle'
|
|
274
|
+
);
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
db.exec(`
|
|
278
|
+
CREATE TABLE IF NOT EXISTS analysis_state (
|
|
279
|
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
280
|
+
status TEXT DEFAULT 'idle',
|
|
281
|
+
current_date TEXT,
|
|
282
|
+
analyzed_count INTEGER DEFAULT 0,
|
|
283
|
+
total_count INTEGER DEFAULT 0,
|
|
284
|
+
last_analyzed_at TEXT
|
|
285
|
+
);
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
// ------------------------------------------------------------------
|
|
289
|
+
// §5.4 Advice execution layer
|
|
290
|
+
//
|
|
291
|
+
// Per-AdviceItem runs of opencode/claude inside the session's project
|
|
292
|
+
// directory. See server/execution/job.js and
|
|
293
|
+
// docs/superpowers/specs/2026-06-13-advice-execution-design.md.
|
|
294
|
+
// ------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
db.exec(`
|
|
297
|
+
CREATE TABLE IF NOT EXISTS execution_run (
|
|
298
|
+
id TEXT PRIMARY KEY,
|
|
299
|
+
session_id TEXT NOT NULL,
|
|
300
|
+
advice_key TEXT NOT NULL,
|
|
301
|
+
advice_snapshot TEXT NOT NULL,
|
|
302
|
+
project TEXT NOT NULL,
|
|
303
|
+
executor TEXT NOT NULL,
|
|
304
|
+
status TEXT NOT NULL,
|
|
305
|
+
started_at TEXT,
|
|
306
|
+
ended_at TEXT,
|
|
307
|
+
exit_code INTEGER,
|
|
308
|
+
stdout TEXT,
|
|
309
|
+
stderr TEXT,
|
|
310
|
+
error TEXT,
|
|
311
|
+
duration_ms INTEGER
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
CREATE INDEX IF NOT EXISTS idx_execution_run_session
|
|
315
|
+
ON execution_run(session_id, advice_key, started_at DESC);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_execution_run_status
|
|
317
|
+
ON execution_run(status);
|
|
318
|
+
`);
|
|
319
|
+
|
|
320
|
+
// ----- Seed default rows -----
|
|
321
|
+
|
|
322
|
+
const defaultSettings = [
|
|
323
|
+
['hourly_rate', '0'],
|
|
324
|
+
['display_currency', 'USD'],
|
|
325
|
+
['currency_rate', '1'],
|
|
326
|
+
['idle_threshold_minutes', '5'],
|
|
327
|
+
['llm_tool_preference', 'auto'],
|
|
328
|
+
// v2: opt-in LLM judge for E1/O1 dimensions
|
|
329
|
+
['enable_llm_judge', '0'],
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const stmt = db.prepare(
|
|
333
|
+
'INSERT OR IGNORE INTO user_settings (key, value) VALUES (?, ?)'
|
|
334
|
+
);
|
|
335
|
+
for (const [key, value] of defaultSettings) {
|
|
336
|
+
stmt.bind([key, value]);
|
|
337
|
+
stmt.step();
|
|
338
|
+
stmt.reset();
|
|
339
|
+
}
|
|
340
|
+
stmt.free();
|
|
341
|
+
|
|
342
|
+
// Default analysis state row
|
|
343
|
+
db.run(
|
|
344
|
+
'INSERT OR IGNORE INTO analysis_state (id, status) VALUES (?, ?)',
|
|
345
|
+
[1, 'idle']
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// ------------------------------------------------------------------
|
|
349
|
+
// §5.4 v2 capability-model additions (additive; tolerated on legacy)
|
|
350
|
+
// ------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
applyV2Migrations(db);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Migration helpers
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Add a column to an existing table only when missing. Uses
|
|
361
|
+
* `PRAGMA table_info` to discover existing columns. This is the cheapest
|
|
362
|
+
* possible migration runner — good enough while the project is pre-1.0
|
|
363
|
+
* and the only writer is a single in-memory sql.js instance.
|
|
364
|
+
*
|
|
365
|
+
* @param {object} db sql.js database
|
|
366
|
+
* @param {string} table table name
|
|
367
|
+
* @param {string} col column name to add
|
|
368
|
+
* @param {string} ddl the column DDL fragment, e.g. "TEXT DEFAULT NULL"
|
|
369
|
+
*/
|
|
370
|
+
function ensureColumn(db, table, col, ddl) {
|
|
371
|
+
const res = db.exec(`PRAGMA table_info(${table})`);
|
|
372
|
+
if (!res || !res[0]) return;
|
|
373
|
+
const cols = res[0].values.map((r) => r[1]);
|
|
374
|
+
if (cols.includes(col)) return;
|
|
375
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${ddl}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Add columns introduced for the v2 capability model. */
|
|
379
|
+
function applyV2Migrations(db) {
|
|
380
|
+
// unified_message: store text payload so text-based signals can run
|
|
381
|
+
// without re-opening the source DB. Defaults to NULL on legacy rows.
|
|
382
|
+
ensureColumn(db, 'unified_message', 'text', 'TEXT');
|
|
383
|
+
|
|
384
|
+
// unified_session: track which sessions are subagents (delegated by a
|
|
385
|
+
// parent task call) and what flavour they are ('build' / 'explore' /
|
|
386
|
+
// null for top-level). Used so the "会话列表" views hide subagents by
|
|
387
|
+
// default while statistics (cost / token / advice generation) still
|
|
388
|
+
// include them. Legacy rows have NULL parent_session_id; a startup
|
|
389
|
+
// backfill (server/etl/backfill-subagents.js) repairs them from the
|
|
390
|
+
// source opencode.db.
|
|
391
|
+
ensureColumn(db, 'unified_session', 'parent_session_id', 'TEXT');
|
|
392
|
+
ensureColumn(db, 'unified_session', 'agent_type', 'TEXT');
|
|
393
|
+
db.exec(`
|
|
394
|
+
CREATE INDEX IF NOT EXISTS idx_unified_session_parent
|
|
395
|
+
ON unified_session(parent_session_id);
|
|
396
|
+
`);
|
|
397
|
+
|
|
398
|
+
// session_analysis: v2 scores per dimension + difficulty + LLM cache.
|
|
399
|
+
ensureColumn(db, 'session_analysis', 'difficulty', 'INTEGER');
|
|
400
|
+
ensureColumn(db, 'session_analysis', 'score_h1', 'REAL');
|
|
401
|
+
ensureColumn(db, 'session_analysis', 'level_h1', 'INTEGER');
|
|
402
|
+
ensureColumn(db, 'session_analysis', 'score_h2', 'REAL');
|
|
403
|
+
ensureColumn(db, 'session_analysis', 'level_h2', 'INTEGER');
|
|
404
|
+
ensureColumn(db, 'session_analysis', 'score_h3', 'REAL');
|
|
405
|
+
ensureColumn(db, 'session_analysis', 'level_h3', 'INTEGER');
|
|
406
|
+
ensureColumn(db, 'session_analysis', 'score_e1', 'REAL');
|
|
407
|
+
ensureColumn(db, 'session_analysis', 'level_e1', 'INTEGER');
|
|
408
|
+
ensureColumn(db, 'session_analysis', 'score_e2', 'REAL');
|
|
409
|
+
ensureColumn(db, 'session_analysis', 'level_e2', 'INTEGER');
|
|
410
|
+
ensureColumn(db, 'session_analysis', 'score_o1', 'REAL');
|
|
411
|
+
ensureColumn(db, 'session_analysis', 'level_o1', 'INTEGER');
|
|
412
|
+
ensureColumn(db, 'session_analysis', 'sub_scores_v2', 'TEXT');
|
|
413
|
+
ensureColumn(db, 'session_analysis', 'llm_judge_v2', 'TEXT');
|
|
414
|
+
ensureColumn(db, 'session_analysis', 'judge_source', 'TEXT');
|
|
415
|
+
|
|
416
|
+
// Per-session AI advice (cost / accuracy / context / skills / workflow).
|
|
417
|
+
// Stores the latest llm-generated suggestion blob. Versioned via
|
|
418
|
+
// ADVICE_PROMPT_VERSION inside the JSON payload — bumping that constant
|
|
419
|
+
// makes old caches self-invalidate without a schema change. See
|
|
420
|
+
// server/llm/advice-prompt.js and docs/superpowers/specs/2026-06-13-session-advice-design.md.
|
|
421
|
+
ensureColumn(db, 'session_analysis', 'llm_advice', 'TEXT');
|
|
422
|
+
|
|
423
|
+
// -- execution_run: scope-extended for project-level executions --
|
|
424
|
+
//
|
|
425
|
+
// Original rows store a session_id and look up the AdviceItem in
|
|
426
|
+
// session_analysis.llm_advice. Project-level runs use scope='project',
|
|
427
|
+
// a synthetic key in scope_id (canonical project path + scope details),
|
|
428
|
+
// and look up the AdviceItem in project_advice.llm_advice. Older rows
|
|
429
|
+
// (from before this migration) implicitly have scope='session' /
|
|
430
|
+
// scope_id=session_id; the read path treats NULL scope as 'session'.
|
|
431
|
+
ensureColumn(db, 'execution_run', 'scope', "TEXT"); // 'session' | 'project'
|
|
432
|
+
ensureColumn(db, 'execution_run', 'scope_id', 'TEXT'); // session_id OR canonical project path
|
|
433
|
+
ensureColumn(db, 'execution_run', 'scope_meta', 'TEXT'); // JSON {scope:'project', period, from, to} for project runs
|
|
434
|
+
db.exec(`
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_execution_run_scope
|
|
436
|
+
ON execution_run(scope, scope_id, advice_key, started_at DESC);
|
|
437
|
+
`);
|
|
438
|
+
|
|
439
|
+
// Project-level AI advice (cross-session meta-analysis). Aggregates the
|
|
440
|
+
// already-cached per-session llm_advice payloads under a given project
|
|
441
|
+
// for a given window into a single new payload. PK is project + scope
|
|
442
|
+
// + window_from + window_to so the same project can be analysed multiple
|
|
443
|
+
// times (e.g. yesterday window AND weekly window) without colliding.
|
|
444
|
+
//
|
|
445
|
+
// See server/llm/project-advice.js and the API at /api/project/:key/advice.
|
|
446
|
+
db.exec(`
|
|
447
|
+
CREATE TABLE IF NOT EXISTS project_advice (
|
|
448
|
+
project TEXT NOT NULL,
|
|
449
|
+
scope TEXT NOT NULL, -- 'daily' | 'weekly' | 'all'
|
|
450
|
+
window_from TEXT NOT NULL, -- inclusive YYYY-MM-DD ('' when scope='all')
|
|
451
|
+
window_to TEXT NOT NULL, -- inclusive YYYY-MM-DD ('' when scope='all')
|
|
452
|
+
session_count INTEGER DEFAULT 0,
|
|
453
|
+
session_ids TEXT, -- JSON array of session ids used
|
|
454
|
+
llm_advice TEXT, -- JSON payload (same shape as session llm_advice + crossSessionPatterns)
|
|
455
|
+
v INTEGER, -- PROJECT_ADVICE_PROMPT_VERSION at time of generation
|
|
456
|
+
cli TEXT,
|
|
457
|
+
cached_at TEXT,
|
|
458
|
+
PRIMARY KEY (project, scope, window_from, window_to)
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
CREATE INDEX IF NOT EXISTS idx_project_advice_project
|
|
462
|
+
ON project_advice(project);
|
|
463
|
+
`);
|
|
464
|
+
|
|
465
|
+
// Rolling capability aggregate (H3 lives here).
|
|
466
|
+
db.exec(`
|
|
467
|
+
CREATE TABLE IF NOT EXISTS capability_rollup_v2 (
|
|
468
|
+
id TEXT PRIMARY KEY,
|
|
469
|
+
period TEXT NOT NULL,
|
|
470
|
+
end_date TEXT NOT NULL,
|
|
471
|
+
window_days INTEGER NOT NULL,
|
|
472
|
+
score_h1 REAL, level_h1 INTEGER,
|
|
473
|
+
score_h2 REAL, level_h2 INTEGER,
|
|
474
|
+
score_h3 REAL, level_h3 INTEGER,
|
|
475
|
+
score_e1 REAL, level_e1 INTEGER,
|
|
476
|
+
score_e2 REAL, level_e2 INTEGER,
|
|
477
|
+
score_o1 REAL, level_o1 INTEGER,
|
|
478
|
+
sub_scores_v2 TEXT,
|
|
479
|
+
computed_at TEXT
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_capability_rollup_v2_end
|
|
483
|
+
ON capability_rollup_v2(period, end_date);
|
|
484
|
+
`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
module.exports = { initDatabase, getDbPath, applyV2Migrations };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active-time calculation for Agent Boss sessions (design doc §4.8)
|
|
3
|
+
*
|
|
4
|
+
* Algorithm:
|
|
5
|
+
* 1. Take all messages for a session from unified_message, ordered by timestamp
|
|
6
|
+
* 2. Calculate time intervals between adjacent messages
|
|
7
|
+
* 3. Intervals <= IDLE_THRESHOLD (default 5 min) are "active"
|
|
8
|
+
* 4. Intervals > IDLE_THRESHOLD are "idle" and excluded
|
|
9
|
+
* 5. active_minutes = SUM(active intervals) + end cap (1 min, capped)
|
|
10
|
+
*
|
|
11
|
+
* @author Felix
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { getSetting, queryAll } = require('../db/queries');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Constants
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** Default idle threshold in minutes */
|
|
21
|
+
const DEFAULT_IDLE_THRESHOLD_MINUTES = 5;
|
|
22
|
+
|
|
23
|
+
/** End cap added after the last active interval (minutes) */
|
|
24
|
+
const END_CAP_MINUTES = 1;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Pure computation helper
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate active minutes for a single session given its messages.
|
|
32
|
+
*
|
|
33
|
+
* @param {Array<{timestamp: string}>} messages - array of objects with ISO 8601
|
|
34
|
+
* timestamp strings, sorted ASC
|
|
35
|
+
* @param {number} idleThresholdMs - threshold in milliseconds; intervals longer
|
|
36
|
+
* than this are considered idle
|
|
37
|
+
* @returns {number} active minutes (integer, >= 0)
|
|
38
|
+
*/
|
|
39
|
+
function computeActiveMinutes(messages, idleThresholdMs) {
|
|
40
|
+
if (!messages || messages.length === 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (messages.length === 1) {
|
|
44
|
+
return 1; // minimum for a single-message session
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse timestamps once
|
|
48
|
+
const times = messages.map((m) => new Date(m.timestamp).getTime());
|
|
49
|
+
|
|
50
|
+
let activeMs = 0;
|
|
51
|
+
|
|
52
|
+
for (let i = 1; i < times.length; i++) {
|
|
53
|
+
const interval = times[i] - times[i - 1];
|
|
54
|
+
if (interval <= idleThresholdMs) {
|
|
55
|
+
activeMs += interval;
|
|
56
|
+
}
|
|
57
|
+
// idle intervals are simply skipped
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// End cap: add 1 minute, but don't exceed the last interval if it's < 1 min
|
|
61
|
+
const lastInterval = times[times.length - 1] - times[times.length - 2];
|
|
62
|
+
const endCapMs = END_CAP_MINUTES * 60 * 1000;
|
|
63
|
+
const cappedEndMs = Math.min(endCapMs, lastInterval);
|
|
64
|
+
activeMs += cappedEndMs;
|
|
65
|
+
|
|
66
|
+
// Convert to integer minutes (ceil so sub-minute activity isn't lost)
|
|
67
|
+
return Math.ceil(activeMs / 60000);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Main batch function
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Calculate active_minutes for all sessions that don't have it yet.
|
|
76
|
+
*
|
|
77
|
+
* Resolution order for idle threshold:
|
|
78
|
+
* 1. options.idleThresholdMinutes (caller override)
|
|
79
|
+
* 2. user_settings row with key 'idle_threshold_minutes'
|
|
80
|
+
* 3. DEFAULT_IDLE_THRESHOLD_MINUTES (5)
|
|
81
|
+
*
|
|
82
|
+
* @param {object} db - sql.js database instance (boss.db)
|
|
83
|
+
* @param {object} [options] - { idleThresholdMinutes: number }
|
|
84
|
+
* @returns {number} count of sessions updated
|
|
85
|
+
*/
|
|
86
|
+
function calculateActiveTime(db, options = {}) {
|
|
87
|
+
// --- resolve idle threshold ---
|
|
88
|
+
let thresholdMinutes = options.idleThresholdMinutes;
|
|
89
|
+
|
|
90
|
+
if (thresholdMinutes == null) {
|
|
91
|
+
const settingValue = getSetting(db, 'idle_threshold_minutes');
|
|
92
|
+
if (settingValue != null) {
|
|
93
|
+
thresholdMinutes = Number(settingValue);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (thresholdMinutes == null || Number.isNaN(thresholdMinutes)) {
|
|
98
|
+
thresholdMinutes = DEFAULT_IDLE_THRESHOLD_MINUTES;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const idleThresholdMs = thresholdMinutes * 60 * 1000;
|
|
102
|
+
|
|
103
|
+
// --- find sessions that still need active_minutes ---
|
|
104
|
+
const sessions = queryAll(
|
|
105
|
+
db,
|
|
106
|
+
'SELECT id FROM unified_session WHERE active_minutes IS NULL'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (sessions.length === 0) {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- process each session inside a single transaction ---
|
|
114
|
+
let updatedCount = 0;
|
|
115
|
+
|
|
116
|
+
db.run('BEGIN TRANSACTION');
|
|
117
|
+
try {
|
|
118
|
+
for (const session of sessions) {
|
|
119
|
+
const messages = queryAll(
|
|
120
|
+
db,
|
|
121
|
+
'SELECT timestamp FROM unified_message WHERE session_id = ? ORDER BY timestamp ASC',
|
|
122
|
+
[session.id]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const activeMinutes = computeActiveMinutes(messages, idleThresholdMs);
|
|
126
|
+
|
|
127
|
+
db.run(
|
|
128
|
+
'UPDATE unified_session SET active_minutes = ? WHERE id = ?',
|
|
129
|
+
[activeMinutes, session.id]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
updatedCount++;
|
|
133
|
+
}
|
|
134
|
+
db.run('COMMIT');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
db.run('ROLLBACK');
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return updatedCount;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Exports
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
calculateActiveTime,
|
|
149
|
+
computeActiveMinutes,
|
|
150
|
+
};
|