selftune 0.2.15 → 0.2.18
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 +24 -19
- package/bin/run-hook.cjs +36 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +14 -1
- package/cli/selftune/alpha-upload/client.ts +51 -1
- package/cli/selftune/alpha-upload/flush.ts +46 -5
- package/cli/selftune/alpha-upload/stage-canonical.ts +25 -4
- package/cli/selftune/alpha-upload-contract.ts +9 -0
- package/cli/selftune/constants.ts +82 -5
- package/cli/selftune/contribute/sanitize.ts +52 -5
- package/cli/selftune/dashboard-contract.ts +100 -0
- package/cli/selftune/dashboard-server.ts +2 -2
- package/cli/selftune/evolution/description-quality.ts +12 -11
- package/cli/selftune/evolution/evolve.ts +238 -53
- package/cli/selftune/evolution/unblock-suggestions.ts +159 -0
- package/cli/selftune/evolution/validate-proposal.ts +9 -6
- package/cli/selftune/grading/grade-session.ts +20 -0
- package/cli/selftune/hooks/commit-track.ts +188 -0
- package/cli/selftune/hooks/prompt-log.ts +10 -1
- package/cli/selftune/hooks/session-stop.ts +2 -2
- package/cli/selftune/hooks/skill-eval.ts +15 -1
- package/cli/selftune/hooks/stdin-preview.ts +32 -0
- package/cli/selftune/init.ts +198 -27
- package/cli/selftune/localdb/direct-write.ts +69 -6
- package/cli/selftune/localdb/queries.ts +552 -7
- package/cli/selftune/localdb/schema.ts +46 -0
- package/cli/selftune/orchestrate.ts +32 -4
- package/cli/selftune/routes/overview.ts +41 -3
- package/cli/selftune/routes/skill-report.ts +88 -17
- package/cli/selftune/types.ts +32 -0
- package/cli/selftune/utils/hooks.ts +12 -2
- package/cli/selftune/utils/transcript.ts +210 -1
- package/node_modules/@selftune/telemetry-contract/src/types.ts +11 -0
- package/package.json +1 -1
- package/packages/telemetry-contract/src/types.ts +11 -0
- package/skill/SKILL.md +29 -1
- package/skill/Workflows/AutoActivation.md +1 -1
- package/skill/Workflows/Evolve.md +31 -13
- package/skill/Workflows/ExportCanonical.md +121 -0
- package/skill/Workflows/Hook.md +131 -0
- package/skill/Workflows/Initialize.md +9 -8
- package/skill/Workflows/Orchestrate.md +27 -5
- package/skill/Workflows/Quickstart.md +94 -0
- package/skill/Workflows/RepairSkillUsage.md +87 -0
- package/skill/Workflows/Uninstall.md +82 -0
- package/skill/settings_snippet.json +19 -8
|
@@ -8,12 +8,21 @@
|
|
|
8
8
|
import type { Database } from "bun:sqlite";
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
+
CommitRecord,
|
|
12
|
+
CommitSummary,
|
|
13
|
+
ExecutionMetrics,
|
|
11
14
|
OrchestrateRunReport,
|
|
15
|
+
OverviewPaginatedPayload,
|
|
12
16
|
OverviewPayload,
|
|
17
|
+
PaginatedResult,
|
|
18
|
+
PaginationCursor,
|
|
13
19
|
PendingProposal,
|
|
14
20
|
RecentActivityItem,
|
|
21
|
+
SkillReportPaginatedPayload,
|
|
15
22
|
SkillReportPayload,
|
|
16
23
|
SkillSummary,
|
|
24
|
+
SkillUsageRecord,
|
|
25
|
+
TelemetryRecord,
|
|
17
26
|
} from "../dashboard-contract.js";
|
|
18
27
|
|
|
19
28
|
/**
|
|
@@ -243,6 +252,386 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
|
|
|
243
252
|
};
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
// -- Cursor-based paginated queries -------------------------------------------
|
|
256
|
+
|
|
257
|
+
export interface OverviewPaginationOptions {
|
|
258
|
+
telemetry_cursor?: PaginationCursor | null;
|
|
259
|
+
telemetry_limit?: number;
|
|
260
|
+
skills_cursor?: PaginationCursor | null;
|
|
261
|
+
skills_limit?: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface SkillReportPaginationOptions {
|
|
265
|
+
invocations_cursor?: PaginationCursor | null;
|
|
266
|
+
invocations_limit?: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Build a paginated overview payload from SQLite.
|
|
271
|
+
*
|
|
272
|
+
* Uses (timestamp, session_id) composite cursors for stable backward pagination.
|
|
273
|
+
* When no cursor is provided, returns the first page starting from most recent.
|
|
274
|
+
*/
|
|
275
|
+
export function getOverviewPayloadPaginated(
|
|
276
|
+
db: Database,
|
|
277
|
+
opts: OverviewPaginationOptions = {},
|
|
278
|
+
): OverviewPaginatedPayload {
|
|
279
|
+
const telemetryLimit = opts.telemetry_limit ?? 1000;
|
|
280
|
+
const skillsLimit = opts.skills_limit ?? 2000;
|
|
281
|
+
|
|
282
|
+
// Paginated telemetry
|
|
283
|
+
const telemetry_page = paginateTelemetry(db, telemetryLimit, opts.telemetry_cursor ?? null);
|
|
284
|
+
|
|
285
|
+
// Paginated skill invocations
|
|
286
|
+
const skills_page = paginateSkillInvocations(db, skillsLimit, opts.skills_cursor ?? null);
|
|
287
|
+
|
|
288
|
+
// Non-paginated parts reuse existing logic
|
|
289
|
+
const evolution = db
|
|
290
|
+
.query(
|
|
291
|
+
`SELECT timestamp, proposal_id, skill_name, action, details
|
|
292
|
+
FROM evolution_audit
|
|
293
|
+
ORDER BY timestamp DESC
|
|
294
|
+
LIMIT 500`,
|
|
295
|
+
)
|
|
296
|
+
.all() as Array<{
|
|
297
|
+
timestamp: string;
|
|
298
|
+
proposal_id: string;
|
|
299
|
+
skill_name: string | null;
|
|
300
|
+
action: string;
|
|
301
|
+
details: string;
|
|
302
|
+
}>;
|
|
303
|
+
|
|
304
|
+
const counts = db
|
|
305
|
+
.query(
|
|
306
|
+
`SELECT
|
|
307
|
+
(SELECT COUNT(*) FROM session_telemetry) as telemetry,
|
|
308
|
+
(SELECT COUNT(*) FROM skill_invocations) as skills,
|
|
309
|
+
(SELECT COUNT(*) FROM evolution_audit) as evolution,
|
|
310
|
+
(SELECT COUNT(*) FROM evolution_evidence) as evidence,
|
|
311
|
+
(SELECT COUNT(*) FROM sessions) as sessions,
|
|
312
|
+
(SELECT COUNT(*) FROM prompts) as prompts`,
|
|
313
|
+
)
|
|
314
|
+
.get() as {
|
|
315
|
+
telemetry: number;
|
|
316
|
+
skills: number;
|
|
317
|
+
evolution: number;
|
|
318
|
+
evidence: number;
|
|
319
|
+
sessions: number;
|
|
320
|
+
prompts: number;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const unmatchedRows = db
|
|
324
|
+
.query(
|
|
325
|
+
`SELECT si.occurred_at AS timestamp, si.session_id, si.query
|
|
326
|
+
FROM skill_invocations si
|
|
327
|
+
WHERE si.triggered = 0
|
|
328
|
+
AND NOT EXISTS (
|
|
329
|
+
SELECT 1 FROM skill_invocations si2
|
|
330
|
+
WHERE si2.query = si.query AND si2.triggered = 1
|
|
331
|
+
)
|
|
332
|
+
ORDER BY si.occurred_at DESC
|
|
333
|
+
LIMIT 500`,
|
|
334
|
+
)
|
|
335
|
+
.all() as Array<{ timestamp: string; session_id: string; query: string }>;
|
|
336
|
+
|
|
337
|
+
const pending_proposals = getPendingProposals(db);
|
|
338
|
+
const active_sessions = getActiveSessionCount(db);
|
|
339
|
+
const recent_activity = getRecentActivity(db);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
telemetry_page,
|
|
343
|
+
skills_page,
|
|
344
|
+
evolution,
|
|
345
|
+
counts,
|
|
346
|
+
unmatched_queries: unmatchedRows,
|
|
347
|
+
pending_proposals,
|
|
348
|
+
active_sessions,
|
|
349
|
+
recent_activity,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Build a paginated skill report payload for a specific skill.
|
|
355
|
+
*
|
|
356
|
+
* Uses (occurred_at, skill_invocation_id) composite cursor for the recent
|
|
357
|
+
* invocations sub-query. Non-paginated fields (usage stats, evidence, sessions)
|
|
358
|
+
* are returned in full.
|
|
359
|
+
*/
|
|
360
|
+
export function getSkillReportPayloadPaginated(
|
|
361
|
+
db: Database,
|
|
362
|
+
skillName: string,
|
|
363
|
+
opts: SkillReportPaginationOptions = {},
|
|
364
|
+
): SkillReportPaginatedPayload {
|
|
365
|
+
const invocationsLimit = opts.invocations_limit ?? 100;
|
|
366
|
+
|
|
367
|
+
// Usage stats (unchanged)
|
|
368
|
+
const usageRow = db
|
|
369
|
+
.query(
|
|
370
|
+
`SELECT
|
|
371
|
+
COUNT(*) as total_checks,
|
|
372
|
+
SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
|
|
373
|
+
FROM skill_invocations
|
|
374
|
+
WHERE skill_name = ?`,
|
|
375
|
+
)
|
|
376
|
+
.get(skillName) as { total_checks: number; triggered_count: number };
|
|
377
|
+
|
|
378
|
+
const total = usageRow.total_checks;
|
|
379
|
+
const triggered = usageRow.triggered_count;
|
|
380
|
+
const passRate = total > 0 ? triggered / total : 0;
|
|
381
|
+
|
|
382
|
+
// Paginated invocations
|
|
383
|
+
const invocations_page = paginateSkillReportInvocations(
|
|
384
|
+
db,
|
|
385
|
+
skillName,
|
|
386
|
+
invocationsLimit,
|
|
387
|
+
opts.invocations_cursor ?? null,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Evidence (unchanged)
|
|
391
|
+
const evidenceRows = db
|
|
392
|
+
.query(
|
|
393
|
+
`SELECT proposal_id, target, stage, timestamp, rationale, confidence,
|
|
394
|
+
original_text, proposed_text, validation_json, details, eval_set_json
|
|
395
|
+
FROM evolution_evidence
|
|
396
|
+
WHERE skill_name = ?
|
|
397
|
+
ORDER BY timestamp DESC
|
|
398
|
+
LIMIT 200`,
|
|
399
|
+
)
|
|
400
|
+
.all(skillName) as Array<{
|
|
401
|
+
proposal_id: string;
|
|
402
|
+
target: string;
|
|
403
|
+
stage: string;
|
|
404
|
+
timestamp: string;
|
|
405
|
+
rationale: string | null;
|
|
406
|
+
confidence: number | null;
|
|
407
|
+
original_text: string | null;
|
|
408
|
+
proposed_text: string | null;
|
|
409
|
+
validation_json: string | null;
|
|
410
|
+
details: string | null;
|
|
411
|
+
eval_set_json: string | null;
|
|
412
|
+
}>;
|
|
413
|
+
|
|
414
|
+
const evidence = evidenceRows.map((row) => ({
|
|
415
|
+
proposal_id: row.proposal_id,
|
|
416
|
+
target: row.target,
|
|
417
|
+
stage: row.stage,
|
|
418
|
+
timestamp: row.timestamp,
|
|
419
|
+
rationale: row.rationale,
|
|
420
|
+
confidence: row.confidence,
|
|
421
|
+
original_text: row.original_text,
|
|
422
|
+
proposed_text: row.proposed_text,
|
|
423
|
+
validation: safeParseJson(row.validation_json),
|
|
424
|
+
details: row.details,
|
|
425
|
+
eval_set: safeParseJsonArray<Record<string, unknown>>(row.eval_set_json),
|
|
426
|
+
}));
|
|
427
|
+
|
|
428
|
+
const sessionsRow = db
|
|
429
|
+
.query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
|
|
430
|
+
.get(skillName) as { c: number };
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
skill_name: skillName,
|
|
434
|
+
usage: {
|
|
435
|
+
total_checks: total,
|
|
436
|
+
triggered_count: triggered,
|
|
437
|
+
pass_rate: passRate,
|
|
438
|
+
},
|
|
439
|
+
invocations_page,
|
|
440
|
+
evidence,
|
|
441
|
+
sessions_with_skill: sessionsRow.c,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// -- Internal pagination helpers ------------------------------------------------
|
|
446
|
+
|
|
447
|
+
function paginateTelemetry(
|
|
448
|
+
db: Database,
|
|
449
|
+
limit: number,
|
|
450
|
+
cursor: PaginationCursor | null,
|
|
451
|
+
): PaginatedResult<TelemetryRecord> {
|
|
452
|
+
// Fetch one extra to detect has_more
|
|
453
|
+
const fetchLimit = limit + 1;
|
|
454
|
+
|
|
455
|
+
let rows: Array<{
|
|
456
|
+
timestamp: string;
|
|
457
|
+
session_id: string;
|
|
458
|
+
skills_triggered_json: string | null;
|
|
459
|
+
errors_encountered: number;
|
|
460
|
+
total_tool_calls: number;
|
|
461
|
+
}>;
|
|
462
|
+
|
|
463
|
+
if (cursor) {
|
|
464
|
+
rows = db
|
|
465
|
+
.query(
|
|
466
|
+
`SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
|
|
467
|
+
FROM session_telemetry
|
|
468
|
+
WHERE (timestamp < ? OR (timestamp = ? AND session_id < ?))
|
|
469
|
+
ORDER BY timestamp DESC, session_id DESC
|
|
470
|
+
LIMIT ?`,
|
|
471
|
+
)
|
|
472
|
+
.all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
|
|
473
|
+
} else {
|
|
474
|
+
rows = db
|
|
475
|
+
.query(
|
|
476
|
+
`SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
|
|
477
|
+
FROM session_telemetry
|
|
478
|
+
ORDER BY timestamp DESC, session_id DESC
|
|
479
|
+
LIMIT ?`,
|
|
480
|
+
)
|
|
481
|
+
.all(fetchLimit) as typeof rows;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const hasMore = rows.length > limit;
|
|
485
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
486
|
+
|
|
487
|
+
const items: TelemetryRecord[] = pageRows.map((row) => ({
|
|
488
|
+
timestamp: row.timestamp,
|
|
489
|
+
session_id: row.session_id,
|
|
490
|
+
skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
|
|
491
|
+
errors_encountered: row.errors_encountered,
|
|
492
|
+
total_tool_calls: row.total_tool_calls,
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
const lastItem = pageRows[pageRows.length - 1];
|
|
496
|
+
const next_cursor: PaginationCursor | null =
|
|
497
|
+
hasMore && lastItem ? { timestamp: lastItem.timestamp, id: lastItem.session_id } : null;
|
|
498
|
+
|
|
499
|
+
return { items, next_cursor, has_more: hasMore };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function paginateSkillInvocations(
|
|
503
|
+
db: Database,
|
|
504
|
+
limit: number,
|
|
505
|
+
cursor: PaginationCursor | null,
|
|
506
|
+
): PaginatedResult<SkillUsageRecord> {
|
|
507
|
+
const fetchLimit = limit + 1;
|
|
508
|
+
|
|
509
|
+
let rows: Array<{
|
|
510
|
+
occurred_at: string;
|
|
511
|
+
session_id: string;
|
|
512
|
+
skill_name: string;
|
|
513
|
+
skill_path: string;
|
|
514
|
+
query: string;
|
|
515
|
+
triggered: number;
|
|
516
|
+
source: string | null;
|
|
517
|
+
skill_invocation_id: string;
|
|
518
|
+
}>;
|
|
519
|
+
|
|
520
|
+
if (cursor) {
|
|
521
|
+
rows = db
|
|
522
|
+
.query(
|
|
523
|
+
`SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
|
|
524
|
+
FROM skill_invocations
|
|
525
|
+
WHERE (occurred_at < ? OR (occurred_at = ? AND skill_invocation_id < ?))
|
|
526
|
+
ORDER BY occurred_at DESC, skill_invocation_id DESC
|
|
527
|
+
LIMIT ?`,
|
|
528
|
+
)
|
|
529
|
+
.all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
|
|
530
|
+
} else {
|
|
531
|
+
rows = db
|
|
532
|
+
.query(
|
|
533
|
+
`SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
|
|
534
|
+
FROM skill_invocations
|
|
535
|
+
ORDER BY occurred_at DESC, skill_invocation_id DESC
|
|
536
|
+
LIMIT ?`,
|
|
537
|
+
)
|
|
538
|
+
.all(fetchLimit) as typeof rows;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const hasMore = rows.length > limit;
|
|
542
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
543
|
+
|
|
544
|
+
const items: SkillUsageRecord[] = pageRows.map((row) => ({
|
|
545
|
+
timestamp: row.occurred_at,
|
|
546
|
+
session_id: row.session_id,
|
|
547
|
+
skill_name: row.skill_name,
|
|
548
|
+
skill_path: row.skill_path,
|
|
549
|
+
query: row.query,
|
|
550
|
+
triggered: row.triggered === 1,
|
|
551
|
+
source: row.source,
|
|
552
|
+
}));
|
|
553
|
+
|
|
554
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
555
|
+
const next_cursor: PaginationCursor | null =
|
|
556
|
+
hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
|
|
557
|
+
|
|
558
|
+
return { items, next_cursor, has_more: hasMore };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function paginateSkillReportInvocations(
|
|
562
|
+
db: Database,
|
|
563
|
+
skillName: string,
|
|
564
|
+
limit: number,
|
|
565
|
+
cursor: PaginationCursor | null,
|
|
566
|
+
): PaginatedResult<{
|
|
567
|
+
timestamp: string;
|
|
568
|
+
session_id: string;
|
|
569
|
+
query: string;
|
|
570
|
+
triggered: boolean;
|
|
571
|
+
source: string | null;
|
|
572
|
+
}> {
|
|
573
|
+
const fetchLimit = limit + 1;
|
|
574
|
+
|
|
575
|
+
let rows: Array<{
|
|
576
|
+
occurred_at: string;
|
|
577
|
+
session_id: string;
|
|
578
|
+
query: string;
|
|
579
|
+
triggered: number;
|
|
580
|
+
source: string | null;
|
|
581
|
+
skill_invocation_id: string;
|
|
582
|
+
}>;
|
|
583
|
+
|
|
584
|
+
if (cursor) {
|
|
585
|
+
rows = db
|
|
586
|
+
.query(
|
|
587
|
+
`SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
|
|
588
|
+
si.triggered, si.source, si.skill_invocation_id
|
|
589
|
+
FROM skill_invocations si
|
|
590
|
+
LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
|
|
591
|
+
WHERE si.skill_name = ?
|
|
592
|
+
AND (si.occurred_at < ? OR (si.occurred_at = ? AND si.skill_invocation_id < ?))
|
|
593
|
+
ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
|
|
594
|
+
LIMIT ?`,
|
|
595
|
+
)
|
|
596
|
+
.all(
|
|
597
|
+
skillName,
|
|
598
|
+
cursor.timestamp,
|
|
599
|
+
cursor.timestamp,
|
|
600
|
+
String(cursor.id),
|
|
601
|
+
fetchLimit,
|
|
602
|
+
) as typeof rows;
|
|
603
|
+
} else {
|
|
604
|
+
rows = db
|
|
605
|
+
.query(
|
|
606
|
+
`SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
|
|
607
|
+
si.triggered, si.source, si.skill_invocation_id
|
|
608
|
+
FROM skill_invocations si
|
|
609
|
+
LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
|
|
610
|
+
WHERE si.skill_name = ?
|
|
611
|
+
ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
|
|
612
|
+
LIMIT ?`,
|
|
613
|
+
)
|
|
614
|
+
.all(skillName, fetchLimit) as typeof rows;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const hasMore = rows.length > limit;
|
|
618
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
619
|
+
|
|
620
|
+
const items = pageRows.map((row) => ({
|
|
621
|
+
timestamp: row.occurred_at,
|
|
622
|
+
session_id: row.session_id,
|
|
623
|
+
query: row.query ?? "",
|
|
624
|
+
triggered: row.triggered === 1,
|
|
625
|
+
source: row.source,
|
|
626
|
+
}));
|
|
627
|
+
|
|
628
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
629
|
+
const next_cursor: PaginationCursor | null =
|
|
630
|
+
hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
|
|
631
|
+
|
|
632
|
+
return { items, next_cursor, has_more: hasMore };
|
|
633
|
+
}
|
|
634
|
+
|
|
246
635
|
/**
|
|
247
636
|
* Get a summary list of all skills with aggregated stats.
|
|
248
637
|
*/
|
|
@@ -390,13 +779,12 @@ export function getActiveSessionCount(db: Database): number {
|
|
|
390
779
|
* session is still in progress (no session_telemetry row yet).
|
|
391
780
|
*/
|
|
392
781
|
export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[] {
|
|
782
|
+
// Step 1: Fetch recent invocations without JOIN (avoids materializing full JOIN before LIMIT)
|
|
393
783
|
const rows = db
|
|
394
784
|
.query(
|
|
395
|
-
`SELECT
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
LEFT JOIN session_telemetry st ON si.session_id = st.session_id
|
|
399
|
-
ORDER BY si.occurred_at DESC
|
|
785
|
+
`SELECT occurred_at, session_id, skill_name, query, triggered
|
|
786
|
+
FROM skill_invocations
|
|
787
|
+
ORDER BY occurred_at DESC
|
|
400
788
|
LIMIT ?`,
|
|
401
789
|
)
|
|
402
790
|
.all(limit) as Array<{
|
|
@@ -405,16 +793,27 @@ export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[
|
|
|
405
793
|
skill_name: string;
|
|
406
794
|
query: string;
|
|
407
795
|
triggered: number;
|
|
408
|
-
is_live: number;
|
|
409
796
|
}>;
|
|
410
797
|
|
|
798
|
+
if (rows.length === 0) return [];
|
|
799
|
+
|
|
800
|
+
// Step 2: Batch lookup which sessions have completed (have a telemetry row)
|
|
801
|
+
const uniqueSessionIds = [...new Set(rows.map((r) => r.session_id))];
|
|
802
|
+
const placeholders = uniqueSessionIds.map(() => "?").join(",");
|
|
803
|
+
const completedRows = db
|
|
804
|
+
.query(
|
|
805
|
+
`SELECT DISTINCT session_id FROM session_telemetry WHERE session_id IN (${placeholders})`,
|
|
806
|
+
)
|
|
807
|
+
.all(...uniqueSessionIds) as Array<{ session_id: string }>;
|
|
808
|
+
const completedSessions = new Set(completedRows.map((r) => r.session_id));
|
|
809
|
+
|
|
411
810
|
return rows.map((row) => ({
|
|
412
811
|
timestamp: row.occurred_at,
|
|
413
812
|
session_id: row.session_id,
|
|
414
813
|
skill_name: row.skill_name,
|
|
415
814
|
query: row.query ?? "",
|
|
416
815
|
triggered: row.triggered === 1,
|
|
417
|
-
is_live: row.
|
|
816
|
+
is_live: !completedSessions.has(row.session_id),
|
|
418
817
|
}));
|
|
419
818
|
}
|
|
420
819
|
|
|
@@ -914,6 +1313,152 @@ export function getOldestPendingAge(db: Database): number | null {
|
|
|
914
1313
|
}
|
|
915
1314
|
}
|
|
916
1315
|
|
|
1316
|
+
// -- Execution metrics & commit tracking queries ------------------------------
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Aggregate execution_facts enrichment columns for a set of session IDs.
|
|
1320
|
+
*
|
|
1321
|
+
* Returns file change stats, cost totals, token breakdowns, artifact counts,
|
|
1322
|
+
* and session_type distribution across the provided sessions.
|
|
1323
|
+
*/
|
|
1324
|
+
export function getExecutionMetrics(db: Database, sessionIds: string[]): ExecutionMetrics {
|
|
1325
|
+
const empty: ExecutionMetrics = {
|
|
1326
|
+
avg_files_changed: 0,
|
|
1327
|
+
total_lines_added: 0,
|
|
1328
|
+
total_lines_removed: 0,
|
|
1329
|
+
total_cost_usd: 0,
|
|
1330
|
+
avg_cost_usd: 0,
|
|
1331
|
+
cached_input_tokens_total: 0,
|
|
1332
|
+
reasoning_output_tokens_total: 0,
|
|
1333
|
+
artifact_count: 0,
|
|
1334
|
+
session_type_distribution: {},
|
|
1335
|
+
};
|
|
1336
|
+
if (sessionIds.length === 0) return empty;
|
|
1337
|
+
|
|
1338
|
+
const placeholders = sessionIds.map(() => "?").join(",");
|
|
1339
|
+
const row = db
|
|
1340
|
+
.query(
|
|
1341
|
+
`SELECT
|
|
1342
|
+
COALESCE(AVG(files_changed), 0) AS avg_files_changed,
|
|
1343
|
+
COALESCE(SUM(lines_added), 0) AS total_lines_added,
|
|
1344
|
+
COALESCE(SUM(lines_removed), 0) AS total_lines_removed,
|
|
1345
|
+
COALESCE(SUM(cost_usd), 0) AS total_cost_usd,
|
|
1346
|
+
COALESCE(AVG(cost_usd), 0) AS avg_cost_usd,
|
|
1347
|
+
COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens_total,
|
|
1348
|
+
COALESCE(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens_total,
|
|
1349
|
+
COALESCE(SUM(artifact_count), 0) AS artifact_count
|
|
1350
|
+
FROM execution_facts
|
|
1351
|
+
WHERE session_id IN (${placeholders})`,
|
|
1352
|
+
)
|
|
1353
|
+
.get(...sessionIds) as {
|
|
1354
|
+
avg_files_changed: number;
|
|
1355
|
+
total_lines_added: number;
|
|
1356
|
+
total_lines_removed: number;
|
|
1357
|
+
total_cost_usd: number;
|
|
1358
|
+
avg_cost_usd: number;
|
|
1359
|
+
cached_input_tokens_total: number;
|
|
1360
|
+
reasoning_output_tokens_total: number;
|
|
1361
|
+
artifact_count: number;
|
|
1362
|
+
} | null;
|
|
1363
|
+
|
|
1364
|
+
// Session type distribution
|
|
1365
|
+
const typeRows = db
|
|
1366
|
+
.query(
|
|
1367
|
+
`SELECT session_type, COUNT(*) AS count
|
|
1368
|
+
FROM execution_facts
|
|
1369
|
+
WHERE session_id IN (${placeholders}) AND session_type IS NOT NULL
|
|
1370
|
+
GROUP BY session_type`,
|
|
1371
|
+
)
|
|
1372
|
+
.all(...sessionIds) as Array<{ session_type: string; count: number }>;
|
|
1373
|
+
|
|
1374
|
+
const session_type_distribution: Record<string, number> = {};
|
|
1375
|
+
for (const tr of typeRows) {
|
|
1376
|
+
session_type_distribution[tr.session_type] = tr.count;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return {
|
|
1380
|
+
avg_files_changed: row?.avg_files_changed ?? 0,
|
|
1381
|
+
total_lines_added: row?.total_lines_added ?? 0,
|
|
1382
|
+
total_lines_removed: row?.total_lines_removed ?? 0,
|
|
1383
|
+
total_cost_usd: row?.total_cost_usd ?? 0,
|
|
1384
|
+
avg_cost_usd: row?.avg_cost_usd ?? 0,
|
|
1385
|
+
cached_input_tokens_total: row?.cached_input_tokens_total ?? 0,
|
|
1386
|
+
reasoning_output_tokens_total: row?.reasoning_output_tokens_total ?? 0,
|
|
1387
|
+
artifact_count: row?.artifact_count ?? 0,
|
|
1388
|
+
session_type_distribution,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Get all commits tracked for a given session.
|
|
1394
|
+
*/
|
|
1395
|
+
export function getSessionCommits(db: Database, sessionId: string): CommitRecord[] {
|
|
1396
|
+
return db
|
|
1397
|
+
.query(
|
|
1398
|
+
`SELECT commit_sha, commit_title, branch, repo_remote, timestamp
|
|
1399
|
+
FROM commit_tracking
|
|
1400
|
+
WHERE session_id = ?
|
|
1401
|
+
ORDER BY timestamp DESC`,
|
|
1402
|
+
)
|
|
1403
|
+
.all(sessionId) as CommitRecord[];
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Aggregate commit stats for a skill by joining commit_tracking to skill_invocations
|
|
1408
|
+
* via shared session_id.
|
|
1409
|
+
*/
|
|
1410
|
+
export function getSkillCommitSummary(db: Database, skillName: string): CommitSummary {
|
|
1411
|
+
const empty: CommitSummary = {
|
|
1412
|
+
total_commits: 0,
|
|
1413
|
+
unique_branches: 0,
|
|
1414
|
+
recent_commits: [],
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
const statsRow = db
|
|
1418
|
+
.query(
|
|
1419
|
+
`WITH skill_sessions AS (
|
|
1420
|
+
SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
|
|
1421
|
+
)
|
|
1422
|
+
SELECT
|
|
1423
|
+
COUNT(*) AS total_commits,
|
|
1424
|
+
COUNT(DISTINCT ct.branch) AS unique_branches
|
|
1425
|
+
FROM commit_tracking ct
|
|
1426
|
+
WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)`,
|
|
1427
|
+
)
|
|
1428
|
+
.get(skillName) as { total_commits: number; unique_branches: number } | null;
|
|
1429
|
+
|
|
1430
|
+
if (!statsRow || statsRow.total_commits === 0) return empty;
|
|
1431
|
+
|
|
1432
|
+
const recentRows = db
|
|
1433
|
+
.query(
|
|
1434
|
+
`WITH skill_sessions AS (
|
|
1435
|
+
SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
|
|
1436
|
+
)
|
|
1437
|
+
SELECT ct.commit_sha, ct.commit_title, ct.branch, ct.timestamp
|
|
1438
|
+
FROM commit_tracking ct
|
|
1439
|
+
WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)
|
|
1440
|
+
ORDER BY ct.timestamp DESC
|
|
1441
|
+
LIMIT 20`,
|
|
1442
|
+
)
|
|
1443
|
+
.all(skillName) as Array<{
|
|
1444
|
+
commit_sha: string;
|
|
1445
|
+
commit_title: string | null;
|
|
1446
|
+
branch: string | null;
|
|
1447
|
+
timestamp: string;
|
|
1448
|
+
}>;
|
|
1449
|
+
|
|
1450
|
+
return {
|
|
1451
|
+
total_commits: statsRow.total_commits,
|
|
1452
|
+
unique_branches: statsRow.unique_branches,
|
|
1453
|
+
recent_commits: recentRows.map((r) => ({
|
|
1454
|
+
sha: r.commit_sha,
|
|
1455
|
+
title: r.commit_title ?? "",
|
|
1456
|
+
branch: r.branch ?? "",
|
|
1457
|
+
timestamp: r.timestamp,
|
|
1458
|
+
})),
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
917
1462
|
// -- Helpers ------------------------------------------------------------------
|
|
918
1463
|
|
|
919
1464
|
export function safeParseJsonArray<T = string>(json: string | null): T[] {
|
|
@@ -260,6 +260,20 @@ CREATE TABLE IF NOT EXISTS upload_watermarks (
|
|
|
260
260
|
updated_at TEXT NOT NULL
|
|
261
261
|
)`;
|
|
262
262
|
|
|
263
|
+
// -- Commit tracking table ----------------------------------------------------
|
|
264
|
+
|
|
265
|
+
export const CREATE_COMMIT_TRACKING = `
|
|
266
|
+
CREATE TABLE IF NOT EXISTS commit_tracking (
|
|
267
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
268
|
+
session_id TEXT NOT NULL,
|
|
269
|
+
commit_sha TEXT NOT NULL,
|
|
270
|
+
commit_title TEXT,
|
|
271
|
+
branch TEXT,
|
|
272
|
+
repo_remote TEXT,
|
|
273
|
+
timestamp TEXT NOT NULL,
|
|
274
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
275
|
+
)`;
|
|
276
|
+
|
|
263
277
|
// -- Metadata table -----------------------------------------------------------
|
|
264
278
|
|
|
265
279
|
export const CREATE_META = `
|
|
@@ -317,6 +331,11 @@ export const CREATE_INDEXES = [
|
|
|
317
331
|
`CREATE INDEX IF NOT EXISTS idx_staging_kind ON canonical_upload_staging(record_kind)`,
|
|
318
332
|
`CREATE INDEX IF NOT EXISTS idx_staging_session ON canonical_upload_staging(session_id)`,
|
|
319
333
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_staging_dedup ON canonical_upload_staging(record_kind, record_id)`,
|
|
334
|
+
// -- Commit tracking indexes ------------------------------------------------
|
|
335
|
+
`CREATE INDEX IF NOT EXISTS idx_commit_sha ON commit_tracking(commit_sha)`,
|
|
336
|
+
`CREATE INDEX IF NOT EXISTS idx_commit_session ON commit_tracking(session_id)`,
|
|
337
|
+
`CREATE INDEX IF NOT EXISTS idx_commit_ts ON commit_tracking(timestamp)`,
|
|
338
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_commit_dedup ON commit_tracking(session_id, commit_sha)`,
|
|
320
339
|
];
|
|
321
340
|
|
|
322
341
|
/**
|
|
@@ -353,6 +372,31 @@ export const MIGRATIONS = [
|
|
|
353
372
|
`ALTER TABLE execution_facts ADD COLUMN normalizer_version TEXT`,
|
|
354
373
|
`ALTER TABLE execution_facts ADD COLUMN capture_mode TEXT`,
|
|
355
374
|
`ALTER TABLE execution_facts ADD COLUMN raw_source_ref TEXT`,
|
|
375
|
+
// -- Win 2+3: File change metrics + token granularity + cost (execution_facts) --
|
|
376
|
+
`ALTER TABLE execution_facts ADD COLUMN files_changed INTEGER`,
|
|
377
|
+
`ALTER TABLE execution_facts ADD COLUMN lines_added INTEGER`,
|
|
378
|
+
`ALTER TABLE execution_facts ADD COLUMN lines_removed INTEGER`,
|
|
379
|
+
`ALTER TABLE execution_facts ADD COLUMN lines_modified INTEGER`,
|
|
380
|
+
`ALTER TABLE execution_facts ADD COLUMN cached_input_tokens INTEGER`,
|
|
381
|
+
`ALTER TABLE execution_facts ADD COLUMN reasoning_output_tokens INTEGER`,
|
|
382
|
+
`ALTER TABLE execution_facts ADD COLUMN cost_usd REAL`,
|
|
383
|
+
// -- Win 2+3: File change metrics + token granularity + cost (session_telemetry) --
|
|
384
|
+
`ALTER TABLE session_telemetry ADD COLUMN files_changed INTEGER`,
|
|
385
|
+
`ALTER TABLE session_telemetry ADD COLUMN lines_added INTEGER`,
|
|
386
|
+
`ALTER TABLE session_telemetry ADD COLUMN lines_removed INTEGER`,
|
|
387
|
+
`ALTER TABLE session_telemetry ADD COLUMN lines_modified INTEGER`,
|
|
388
|
+
`ALTER TABLE session_telemetry ADD COLUMN cached_input_tokens INTEGER`,
|
|
389
|
+
`ALTER TABLE session_telemetry ADD COLUMN reasoning_output_tokens INTEGER`,
|
|
390
|
+
`ALTER TABLE session_telemetry ADD COLUMN cost_usd REAL`,
|
|
391
|
+
// -- Generalized metrics: artifact count + session type --
|
|
392
|
+
`ALTER TABLE execution_facts ADD COLUMN artifact_count INTEGER`,
|
|
393
|
+
`ALTER TABLE execution_facts ADD COLUMN session_type TEXT`,
|
|
394
|
+
`ALTER TABLE session_telemetry ADD COLUMN artifact_count INTEGER`,
|
|
395
|
+
`ALTER TABLE session_telemetry ADD COLUMN session_type TEXT`,
|
|
396
|
+
// -- Session summary (heuristic, no LLM) --
|
|
397
|
+
`ALTER TABLE session_telemetry ADD COLUMN agent_summary TEXT`,
|
|
398
|
+
// -- SHA256 content hashing for upload dedup --
|
|
399
|
+
`ALTER TABLE canonical_upload_staging ADD COLUMN content_sha256 TEXT`,
|
|
356
400
|
];
|
|
357
401
|
|
|
358
402
|
/** Indexes that depend on migration columns — must run AFTER MIGRATIONS. */
|
|
@@ -360,6 +404,7 @@ export const POST_MIGRATION_INDEXES = [
|
|
|
360
404
|
`CREATE INDEX IF NOT EXISTS idx_skill_inv_query_triggered ON skill_invocations(query, triggered)`,
|
|
361
405
|
`CREATE INDEX IF NOT EXISTS idx_skill_inv_scope ON skill_invocations(skill_name, skill_scope, occurred_at)`,
|
|
362
406
|
`CREATE INDEX IF NOT EXISTS idx_skill_inv_dedup ON skill_invocations(session_id, skill_name, query, occurred_at, triggered)`,
|
|
407
|
+
`CREATE INDEX IF NOT EXISTS idx_staging_sha256 ON canonical_upload_staging(content_sha256)`,
|
|
363
408
|
];
|
|
364
409
|
|
|
365
410
|
/** All DDL statements in creation order. */
|
|
@@ -379,6 +424,7 @@ export const ALL_DDL = [
|
|
|
379
424
|
CREATE_UPLOAD_QUEUE,
|
|
380
425
|
CREATE_UPLOAD_WATERMARKS,
|
|
381
426
|
CREATE_CANONICAL_UPLOAD_STAGING,
|
|
427
|
+
CREATE_COMMIT_TRACKING,
|
|
382
428
|
CREATE_META,
|
|
383
429
|
...CREATE_INDEXES,
|
|
384
430
|
];
|