selftune 0.2.16 → 0.2.19
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 +32 -22
- package/apps/local-dashboard/dist/assets/index-DnhnXQm6.js +60 -0
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- 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 +32 -10
- package/cli/selftune/alpha-upload-contract.ts +9 -0
- package/cli/selftune/constants.ts +92 -5
- package/cli/selftune/contribute/contribute.ts +30 -2
- package/cli/selftune/contribute/sanitize.ts +52 -5
- package/cli/selftune/contribution-config.ts +249 -0
- package/cli/selftune/contribution-relay.ts +177 -0
- package/cli/selftune/contribution-signals.ts +219 -0
- package/cli/selftune/contribution-staging.ts +147 -0
- package/cli/selftune/contributions.ts +532 -0
- package/cli/selftune/creator-contributions.ts +333 -0
- package/cli/selftune/dashboard-contract.ts +305 -1
- package/cli/selftune/dashboard-server.ts +47 -13
- package/cli/selftune/eval/family-overlap.ts +395 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/evolution/description-quality.ts +12 -11
- package/cli/selftune/evolution/evolve.ts +214 -51
- package/cli/selftune/evolution/validate-proposal.ts +9 -6
- package/cli/selftune/export.ts +2 -2
- 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/index.ts +41 -5
- package/cli/selftune/ingestors/codex-rollout.ts +31 -35
- package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
- package/cli/selftune/localdb/db.ts +2 -2
- package/cli/selftune/localdb/direct-write.ts +69 -6
- package/cli/selftune/localdb/queries.ts +1253 -37
- package/cli/selftune/localdb/schema.ts +66 -0
- package/cli/selftune/orchestrate.ts +32 -4
- package/cli/selftune/recover.ts +153 -0
- package/cli/selftune/repair/skill-usage.ts +363 -4
- package/cli/selftune/routes/actions.ts +35 -1
- package/cli/selftune/routes/analytics.ts +14 -0
- package/cli/selftune/routes/index.ts +1 -0
- package/cli/selftune/routes/overview.ts +150 -4
- package/cli/selftune/routes/skill-report.ts +648 -18
- package/cli/selftune/status.ts +81 -2
- package/cli/selftune/sync.ts +56 -2
- package/cli/selftune/trust-model.ts +66 -0
- package/cli/selftune/types.ts +80 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/utils/transcript.ts +210 -1
- package/cli/selftune/watchlist.ts +65 -0
- 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/packages/ui/src/components/ActivityTimeline.tsx +165 -150
- package/packages/ui/src/components/EvidenceViewer.tsx +335 -144
- package/packages/ui/src/components/EvolutionTimeline.tsx +58 -28
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
- package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
- package/packages/ui/src/components/section-cards.tsx +12 -9
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/skill/SKILL.md +40 -2
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +64 -0
- package/skill/Workflows/Contribute.md +6 -3
- package/skill/Workflows/Contributions.md +97 -0
- package/skill/Workflows/CreatorContributions.md +74 -0
- package/skill/Workflows/Dashboard.md +31 -0
- package/skill/Workflows/Evals.md +57 -8
- package/skill/Workflows/Evolve.md +31 -13
- package/skill/Workflows/ExportCanonical.md +121 -0
- package/skill/Workflows/Hook.md +131 -0
- package/skill/Workflows/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +29 -9
- package/skill/Workflows/Orchestrate.md +27 -5
- package/skill/Workflows/Quickstart.md +94 -0
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +95 -0
- package/skill/Workflows/Sync.md +18 -12
- package/skill/Workflows/Uninstall.md +82 -0
- package/skill/settings_snippet.json +11 -0
- package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
- package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
|
@@ -8,12 +8,25 @@
|
|
|
8
8
|
import type { Database } from "bun:sqlite";
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
+
AnalyticsResponse,
|
|
12
|
+
AttentionItem,
|
|
13
|
+
AutonomousDecision,
|
|
14
|
+
CommitRecord,
|
|
15
|
+
CommitSummary,
|
|
16
|
+
DecisionKind,
|
|
17
|
+
ExecutionMetrics,
|
|
11
18
|
OrchestrateRunReport,
|
|
19
|
+
OverviewPaginatedPayload,
|
|
12
20
|
OverviewPayload,
|
|
21
|
+
PaginatedResult,
|
|
22
|
+
PaginationCursor,
|
|
13
23
|
PendingProposal,
|
|
14
24
|
RecentActivityItem,
|
|
25
|
+
SkillReportPaginatedPayload,
|
|
15
26
|
SkillReportPayload,
|
|
16
27
|
SkillSummary,
|
|
28
|
+
SkillUsageRecord,
|
|
29
|
+
TelemetryRecord,
|
|
17
30
|
} from "../dashboard-contract.js";
|
|
18
31
|
|
|
19
32
|
/**
|
|
@@ -243,39 +256,417 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
|
|
|
243
256
|
};
|
|
244
257
|
}
|
|
245
258
|
|
|
259
|
+
// -- Cursor-based paginated queries -------------------------------------------
|
|
260
|
+
|
|
261
|
+
export interface OverviewPaginationOptions {
|
|
262
|
+
telemetry_cursor?: PaginationCursor | null;
|
|
263
|
+
telemetry_limit?: number;
|
|
264
|
+
skills_cursor?: PaginationCursor | null;
|
|
265
|
+
skills_limit?: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SkillReportPaginationOptions {
|
|
269
|
+
invocations_cursor?: PaginationCursor | null;
|
|
270
|
+
invocations_limit?: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
/**
|
|
247
|
-
*
|
|
274
|
+
* Build a paginated overview payload from SQLite.
|
|
275
|
+
*
|
|
276
|
+
* Uses (timestamp, session_id) composite cursors for stable backward pagination.
|
|
277
|
+
* When no cursor is provided, returns the first page starting from most recent.
|
|
248
278
|
*/
|
|
249
|
-
export function
|
|
250
|
-
|
|
279
|
+
export function getOverviewPayloadPaginated(
|
|
280
|
+
db: Database,
|
|
281
|
+
opts: OverviewPaginationOptions = {},
|
|
282
|
+
): OverviewPaginatedPayload {
|
|
283
|
+
const telemetryLimit = opts.telemetry_limit ?? 1000;
|
|
284
|
+
const skillsLimit = opts.skills_limit ?? 2000;
|
|
285
|
+
|
|
286
|
+
// Paginated telemetry
|
|
287
|
+
const telemetry_page = paginateTelemetry(db, telemetryLimit, opts.telemetry_cursor ?? null);
|
|
288
|
+
|
|
289
|
+
// Paginated skill invocations
|
|
290
|
+
const skills_page = paginateSkillInvocations(db, skillsLimit, opts.skills_cursor ?? null);
|
|
291
|
+
|
|
292
|
+
// Non-paginated parts reuse existing logic
|
|
293
|
+
const evolution = db
|
|
294
|
+
.query(
|
|
295
|
+
`SELECT timestamp, proposal_id, skill_name, action, details
|
|
296
|
+
FROM evolution_audit
|
|
297
|
+
ORDER BY timestamp DESC
|
|
298
|
+
LIMIT 500`,
|
|
299
|
+
)
|
|
300
|
+
.all() as Array<{
|
|
301
|
+
timestamp: string;
|
|
302
|
+
proposal_id: string;
|
|
303
|
+
skill_name: string | null;
|
|
304
|
+
action: string;
|
|
305
|
+
details: string;
|
|
306
|
+
}>;
|
|
307
|
+
|
|
308
|
+
const counts = db
|
|
251
309
|
.query(
|
|
252
310
|
`SELECT
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
311
|
+
(SELECT COUNT(*) FROM session_telemetry) as telemetry,
|
|
312
|
+
(SELECT COUNT(*) FROM skill_invocations) as skills,
|
|
313
|
+
(SELECT COUNT(*) FROM evolution_audit) as evolution,
|
|
314
|
+
(SELECT COUNT(*) FROM evolution_evidence) as evidence,
|
|
315
|
+
(SELECT COUNT(*) FROM sessions) as sessions,
|
|
316
|
+
(SELECT COUNT(*) FROM prompts) as prompts`,
|
|
317
|
+
)
|
|
318
|
+
.get() as {
|
|
319
|
+
telemetry: number;
|
|
320
|
+
skills: number;
|
|
321
|
+
evolution: number;
|
|
322
|
+
evidence: number;
|
|
323
|
+
sessions: number;
|
|
324
|
+
prompts: number;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const unmatchedRows = db
|
|
328
|
+
.query(
|
|
329
|
+
`SELECT si.occurred_at AS timestamp, si.session_id, si.query
|
|
266
330
|
FROM skill_invocations si
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
WHERE si.triggered = 0
|
|
332
|
+
AND NOT EXISTS (
|
|
333
|
+
SELECT 1 FROM skill_invocations si2
|
|
334
|
+
WHERE si2.query = si.query AND si2.triggered = 1
|
|
335
|
+
)
|
|
336
|
+
ORDER BY si.occurred_at DESC
|
|
337
|
+
LIMIT 500`,
|
|
269
338
|
)
|
|
270
|
-
.all() as Array<{
|
|
339
|
+
.all() as Array<{ timestamp: string; session_id: string; query: string }>;
|
|
340
|
+
|
|
341
|
+
const pending_proposals = getPendingProposals(db);
|
|
342
|
+
const active_sessions = getActiveSessionCount(db);
|
|
343
|
+
const recent_activity = getRecentActivity(db);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
telemetry_page,
|
|
347
|
+
skills_page,
|
|
348
|
+
evolution,
|
|
349
|
+
counts,
|
|
350
|
+
unmatched_queries: unmatchedRows,
|
|
351
|
+
pending_proposals,
|
|
352
|
+
active_sessions,
|
|
353
|
+
recent_activity,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Build a paginated skill report payload for a specific skill.
|
|
359
|
+
*
|
|
360
|
+
* Uses (occurred_at, skill_invocation_id) composite cursor for the recent
|
|
361
|
+
* invocations sub-query. Non-paginated fields (usage stats, evidence, sessions)
|
|
362
|
+
* are returned in full.
|
|
363
|
+
*/
|
|
364
|
+
export function getSkillReportPayloadPaginated(
|
|
365
|
+
db: Database,
|
|
366
|
+
skillName: string,
|
|
367
|
+
opts: SkillReportPaginationOptions = {},
|
|
368
|
+
): SkillReportPaginatedPayload {
|
|
369
|
+
const invocationsLimit = opts.invocations_limit ?? 100;
|
|
370
|
+
|
|
371
|
+
// Usage stats (unchanged)
|
|
372
|
+
const usageRow = db
|
|
373
|
+
.query(
|
|
374
|
+
`SELECT
|
|
375
|
+
COUNT(*) as total_checks,
|
|
376
|
+
SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
|
|
377
|
+
FROM skill_invocations
|
|
378
|
+
WHERE skill_name = ?`,
|
|
379
|
+
)
|
|
380
|
+
.get(skillName) as { total_checks: number; triggered_count: number };
|
|
381
|
+
|
|
382
|
+
const total = usageRow.total_checks;
|
|
383
|
+
const triggered = usageRow.triggered_count;
|
|
384
|
+
const passRate = total > 0 ? triggered / total : 0;
|
|
385
|
+
|
|
386
|
+
// Paginated invocations
|
|
387
|
+
const invocations_page = paginateSkillReportInvocations(
|
|
388
|
+
db,
|
|
389
|
+
skillName,
|
|
390
|
+
invocationsLimit,
|
|
391
|
+
opts.invocations_cursor ?? null,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Evidence (unchanged)
|
|
395
|
+
const evidenceRows = db
|
|
396
|
+
.query(
|
|
397
|
+
`SELECT proposal_id, target, stage, timestamp, rationale, confidence,
|
|
398
|
+
original_text, proposed_text, validation_json, details, eval_set_json
|
|
399
|
+
FROM evolution_evidence
|
|
400
|
+
WHERE skill_name = ?
|
|
401
|
+
ORDER BY timestamp DESC
|
|
402
|
+
LIMIT 200`,
|
|
403
|
+
)
|
|
404
|
+
.all(skillName) as Array<{
|
|
405
|
+
proposal_id: string;
|
|
406
|
+
target: string;
|
|
407
|
+
stage: string;
|
|
408
|
+
timestamp: string;
|
|
409
|
+
rationale: string | null;
|
|
410
|
+
confidence: number | null;
|
|
411
|
+
original_text: string | null;
|
|
412
|
+
proposed_text: string | null;
|
|
413
|
+
validation_json: string | null;
|
|
414
|
+
details: string | null;
|
|
415
|
+
eval_set_json: string | null;
|
|
416
|
+
}>;
|
|
417
|
+
|
|
418
|
+
const evidence = evidenceRows.map((row) => ({
|
|
419
|
+
proposal_id: row.proposal_id,
|
|
420
|
+
target: row.target,
|
|
421
|
+
stage: row.stage,
|
|
422
|
+
timestamp: row.timestamp,
|
|
423
|
+
rationale: row.rationale,
|
|
424
|
+
confidence: row.confidence,
|
|
425
|
+
original_text: row.original_text,
|
|
426
|
+
proposed_text: row.proposed_text,
|
|
427
|
+
validation: safeParseJson(row.validation_json),
|
|
428
|
+
details: row.details,
|
|
429
|
+
eval_set: safeParseJsonArray<Record<string, unknown>>(row.eval_set_json),
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
const sessionsRow = db
|
|
433
|
+
.query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
|
|
434
|
+
.get(skillName) as { c: number };
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
skill_name: skillName,
|
|
438
|
+
usage: {
|
|
439
|
+
total_checks: total,
|
|
440
|
+
triggered_count: triggered,
|
|
441
|
+
pass_rate: passRate,
|
|
442
|
+
},
|
|
443
|
+
invocations_page,
|
|
444
|
+
evidence,
|
|
445
|
+
sessions_with_skill: sessionsRow.c,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// -- Internal pagination helpers ------------------------------------------------
|
|
450
|
+
|
|
451
|
+
function paginateTelemetry(
|
|
452
|
+
db: Database,
|
|
453
|
+
limit: number,
|
|
454
|
+
cursor: PaginationCursor | null,
|
|
455
|
+
): PaginatedResult<TelemetryRecord> {
|
|
456
|
+
// Fetch one extra to detect has_more
|
|
457
|
+
const fetchLimit = limit + 1;
|
|
458
|
+
|
|
459
|
+
let rows: Array<{
|
|
460
|
+
timestamp: string;
|
|
461
|
+
session_id: string;
|
|
462
|
+
skills_triggered_json: string | null;
|
|
463
|
+
errors_encountered: number;
|
|
464
|
+
total_tool_calls: number;
|
|
465
|
+
}>;
|
|
466
|
+
|
|
467
|
+
if (cursor) {
|
|
468
|
+
rows = db
|
|
469
|
+
.query(
|
|
470
|
+
`SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
|
|
471
|
+
FROM session_telemetry
|
|
472
|
+
WHERE (timestamp < ? OR (timestamp = ? AND session_id < ?))
|
|
473
|
+
ORDER BY timestamp DESC, session_id DESC
|
|
474
|
+
LIMIT ?`,
|
|
475
|
+
)
|
|
476
|
+
.all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
|
|
477
|
+
} else {
|
|
478
|
+
rows = db
|
|
479
|
+
.query(
|
|
480
|
+
`SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
|
|
481
|
+
FROM session_telemetry
|
|
482
|
+
ORDER BY timestamp DESC, session_id DESC
|
|
483
|
+
LIMIT ?`,
|
|
484
|
+
)
|
|
485
|
+
.all(fetchLimit) as typeof rows;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const hasMore = rows.length > limit;
|
|
489
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
490
|
+
|
|
491
|
+
const items: TelemetryRecord[] = pageRows.map((row) => ({
|
|
492
|
+
timestamp: row.timestamp,
|
|
493
|
+
session_id: row.session_id,
|
|
494
|
+
skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
|
|
495
|
+
errors_encountered: row.errors_encountered,
|
|
496
|
+
total_tool_calls: row.total_tool_calls,
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
const lastItem = pageRows[pageRows.length - 1];
|
|
500
|
+
const next_cursor: PaginationCursor | null =
|
|
501
|
+
hasMore && lastItem ? { timestamp: lastItem.timestamp, id: lastItem.session_id } : null;
|
|
502
|
+
|
|
503
|
+
return { items, next_cursor, has_more: hasMore };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function paginateSkillInvocations(
|
|
507
|
+
db: Database,
|
|
508
|
+
limit: number,
|
|
509
|
+
cursor: PaginationCursor | null,
|
|
510
|
+
): PaginatedResult<SkillUsageRecord> {
|
|
511
|
+
const fetchLimit = limit + 1;
|
|
512
|
+
|
|
513
|
+
let rows: Array<{
|
|
514
|
+
occurred_at: string;
|
|
515
|
+
session_id: string;
|
|
271
516
|
skill_name: string;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
517
|
+
skill_path: string;
|
|
518
|
+
query: string;
|
|
519
|
+
triggered: number;
|
|
520
|
+
source: string | null;
|
|
521
|
+
skill_invocation_id: string;
|
|
277
522
|
}>;
|
|
278
523
|
|
|
524
|
+
if (cursor) {
|
|
525
|
+
rows = db
|
|
526
|
+
.query(
|
|
527
|
+
`SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
|
|
528
|
+
FROM skill_invocations
|
|
529
|
+
WHERE (occurred_at < ? OR (occurred_at = ? AND skill_invocation_id < ?))
|
|
530
|
+
ORDER BY occurred_at DESC, skill_invocation_id DESC
|
|
531
|
+
LIMIT ?`,
|
|
532
|
+
)
|
|
533
|
+
.all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
|
|
534
|
+
} else {
|
|
535
|
+
rows = db
|
|
536
|
+
.query(
|
|
537
|
+
`SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
|
|
538
|
+
FROM skill_invocations
|
|
539
|
+
ORDER BY occurred_at DESC, skill_invocation_id DESC
|
|
540
|
+
LIMIT ?`,
|
|
541
|
+
)
|
|
542
|
+
.all(fetchLimit) as typeof rows;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const hasMore = rows.length > limit;
|
|
546
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
547
|
+
|
|
548
|
+
const items: SkillUsageRecord[] = pageRows.map((row) => ({
|
|
549
|
+
timestamp: row.occurred_at,
|
|
550
|
+
session_id: row.session_id,
|
|
551
|
+
skill_name: row.skill_name,
|
|
552
|
+
skill_path: row.skill_path,
|
|
553
|
+
query: row.query,
|
|
554
|
+
triggered: row.triggered === 1,
|
|
555
|
+
source: row.source,
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
559
|
+
const next_cursor: PaginationCursor | null =
|
|
560
|
+
hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
|
|
561
|
+
|
|
562
|
+
return { items, next_cursor, has_more: hasMore };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function paginateSkillReportInvocations(
|
|
566
|
+
db: Database,
|
|
567
|
+
skillName: string,
|
|
568
|
+
limit: number,
|
|
569
|
+
cursor: PaginationCursor | null,
|
|
570
|
+
): PaginatedResult<{
|
|
571
|
+
timestamp: string;
|
|
572
|
+
session_id: string;
|
|
573
|
+
query: string;
|
|
574
|
+
triggered: boolean;
|
|
575
|
+
source: string | null;
|
|
576
|
+
}> {
|
|
577
|
+
const fetchLimit = limit + 1;
|
|
578
|
+
|
|
579
|
+
let rows: Array<{
|
|
580
|
+
occurred_at: string;
|
|
581
|
+
session_id: string;
|
|
582
|
+
query: string;
|
|
583
|
+
triggered: number;
|
|
584
|
+
source: string | null;
|
|
585
|
+
skill_invocation_id: string;
|
|
586
|
+
}>;
|
|
587
|
+
|
|
588
|
+
if (cursor) {
|
|
589
|
+
rows = db
|
|
590
|
+
.query(
|
|
591
|
+
`SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
|
|
592
|
+
si.triggered, si.source, si.skill_invocation_id
|
|
593
|
+
FROM skill_invocations si
|
|
594
|
+
LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
|
|
595
|
+
WHERE si.skill_name = ?
|
|
596
|
+
AND (si.occurred_at < ? OR (si.occurred_at = ? AND si.skill_invocation_id < ?))
|
|
597
|
+
ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
|
|
598
|
+
LIMIT ?`,
|
|
599
|
+
)
|
|
600
|
+
.all(
|
|
601
|
+
skillName,
|
|
602
|
+
cursor.timestamp,
|
|
603
|
+
cursor.timestamp,
|
|
604
|
+
String(cursor.id),
|
|
605
|
+
fetchLimit,
|
|
606
|
+
) as typeof rows;
|
|
607
|
+
} else {
|
|
608
|
+
rows = db
|
|
609
|
+
.query(
|
|
610
|
+
`SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
|
|
611
|
+
si.triggered, si.source, si.skill_invocation_id
|
|
612
|
+
FROM skill_invocations si
|
|
613
|
+
LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
|
|
614
|
+
WHERE si.skill_name = ?
|
|
615
|
+
ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
|
|
616
|
+
LIMIT ?`,
|
|
617
|
+
)
|
|
618
|
+
.all(skillName, fetchLimit) as typeof rows;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const hasMore = rows.length > limit;
|
|
622
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
623
|
+
|
|
624
|
+
const items = pageRows.map((row) => ({
|
|
625
|
+
timestamp: row.occurred_at,
|
|
626
|
+
session_id: row.session_id,
|
|
627
|
+
query: row.query ?? "",
|
|
628
|
+
triggered: row.triggered === 1,
|
|
629
|
+
source: row.source,
|
|
630
|
+
}));
|
|
631
|
+
|
|
632
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
633
|
+
const next_cursor: PaginationCursor | null =
|
|
634
|
+
hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
|
|
635
|
+
|
|
636
|
+
return { items, next_cursor, has_more: hasMore };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get a summary list of all skills with aggregated stats.
|
|
641
|
+
*/
|
|
642
|
+
export function getSkillsList(db: Database): SkillSummary[] {
|
|
643
|
+
const trustedRows = queryTrustedSkillObservationRows(db);
|
|
644
|
+
const bySkill = new Map<
|
|
645
|
+
string,
|
|
646
|
+
Array<{
|
|
647
|
+
skill_name: string;
|
|
648
|
+
session_id: string;
|
|
649
|
+
occurred_at: string | null;
|
|
650
|
+
triggered: number;
|
|
651
|
+
matched_prompt_id: string | null;
|
|
652
|
+
confidence: number | null;
|
|
653
|
+
}>
|
|
654
|
+
>();
|
|
655
|
+
|
|
656
|
+
for (const row of trustedRows) {
|
|
657
|
+
const arr = bySkill.get(row.skill_name);
|
|
658
|
+
const base = {
|
|
659
|
+
skill_name: row.skill_name,
|
|
660
|
+
session_id: row.session_id,
|
|
661
|
+
occurred_at: row.occurred_at,
|
|
662
|
+
triggered: row.triggered,
|
|
663
|
+
matched_prompt_id: row.matched_prompt_id,
|
|
664
|
+
confidence: row.confidence,
|
|
665
|
+
};
|
|
666
|
+
if (arr) arr.push(base);
|
|
667
|
+
else bySkill.set(row.skill_name, [base]);
|
|
668
|
+
}
|
|
669
|
+
|
|
279
670
|
// Get set of skill names with evidence
|
|
280
671
|
const evidenceSkills = new Set(
|
|
281
672
|
(
|
|
@@ -285,16 +676,214 @@ export function getSkillsList(db: Database): SkillSummary[] {
|
|
|
285
676
|
).map((r) => r.skill_name),
|
|
286
677
|
);
|
|
287
678
|
|
|
288
|
-
|
|
679
|
+
const skillScopeRows = db
|
|
680
|
+
.query(
|
|
681
|
+
`SELECT
|
|
682
|
+
si.skill_name,
|
|
683
|
+
COALESCE(
|
|
684
|
+
(SELECT s2.skill_scope FROM skill_invocations s2
|
|
685
|
+
WHERE s2.skill_name = si.skill_name AND s2.skill_scope IS NOT NULL
|
|
686
|
+
ORDER BY s2.occurred_at DESC LIMIT 1),
|
|
687
|
+
(SELECT su.skill_scope FROM skill_usage su
|
|
688
|
+
WHERE su.skill_name = si.skill_name AND su.skill_scope IS NOT NULL
|
|
689
|
+
ORDER BY su.timestamp DESC LIMIT 1)
|
|
690
|
+
) as skill_scope
|
|
691
|
+
FROM skill_invocations si
|
|
692
|
+
GROUP BY si.skill_name`,
|
|
693
|
+
)
|
|
694
|
+
.all() as Array<{ skill_name: string; skill_scope: string | null }>;
|
|
695
|
+
const scopeBySkill = new Map(skillScopeRows.map((row) => [row.skill_name, row.skill_scope]));
|
|
696
|
+
|
|
697
|
+
return [...bySkill.entries()]
|
|
698
|
+
.map(([skillName, rows]) => {
|
|
699
|
+
const totalChecks = rows.length;
|
|
700
|
+
const triggeredCount = rows.filter((row) => row.triggered === 1).length;
|
|
701
|
+
const uniqueSessions = new Set(rows.map((row) => row.session_id)).size;
|
|
702
|
+
const lastSeen =
|
|
703
|
+
rows
|
|
704
|
+
.map((row) => row.occurred_at)
|
|
705
|
+
.filter((value): value is string => value != null)
|
|
706
|
+
.sort((a, b) => b.localeCompare(a))[0] ?? null;
|
|
707
|
+
const withConfidence = rows.filter((row) => row.confidence != null);
|
|
708
|
+
const routingConfidence =
|
|
709
|
+
withConfidence.length > 0
|
|
710
|
+
? withConfidence.reduce((sum, row) => sum + (row.confidence ?? 0), 0) /
|
|
711
|
+
withConfidence.length
|
|
712
|
+
: null;
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
skill_name: skillName,
|
|
716
|
+
skill_scope: scopeBySkill.get(skillName) ?? null,
|
|
717
|
+
total_checks: totalChecks,
|
|
718
|
+
triggered_count: triggeredCount,
|
|
719
|
+
pass_rate: totalChecks > 0 ? triggeredCount / totalChecks : 0,
|
|
720
|
+
unique_sessions: uniqueSessions,
|
|
721
|
+
last_seen: lastSeen,
|
|
722
|
+
has_evidence: evidenceSkills.has(skillName),
|
|
723
|
+
routing_confidence: routingConfidence,
|
|
724
|
+
confidence_coverage: totalChecks > 0 ? withConfidence.length / totalChecks : 0,
|
|
725
|
+
};
|
|
726
|
+
})
|
|
727
|
+
.sort((a, b) => b.total_checks - a.total_checks);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Build the performance analytics payload from SQLite.
|
|
732
|
+
* Powers the GET /api/v2/analytics endpoint.
|
|
733
|
+
*/
|
|
734
|
+
export function getAnalyticsPayload(db: Database): AnalyticsResponse {
|
|
735
|
+
const trustedRows = queryTrustedSkillObservationRows(db);
|
|
736
|
+
const today = new Date();
|
|
737
|
+
const dateKey = (value: string | null): string | null => {
|
|
738
|
+
if (!value) return null;
|
|
739
|
+
const parsed = new Date(value);
|
|
740
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
|
|
741
|
+
};
|
|
742
|
+
const cutoffDate = (days: number): string => {
|
|
743
|
+
const cutoff = new Date(today);
|
|
744
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - days);
|
|
745
|
+
return cutoff.toISOString().slice(0, 10);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// 1. Pass rate trend — last 90 days, bucketed by day
|
|
749
|
+
const passRateTrendByDate = new Map<string, { triggered: number; total: number }>();
|
|
750
|
+
for (const row of trustedRows) {
|
|
751
|
+
const occurredDate = dateKey(row.occurred_at);
|
|
752
|
+
if (!occurredDate || occurredDate < cutoffDate(90)) continue;
|
|
753
|
+
const counts = passRateTrendByDate.get(occurredDate) ?? { triggered: 0, total: 0 };
|
|
754
|
+
counts.total += 1;
|
|
755
|
+
if (row.triggered === 1) counts.triggered += 1;
|
|
756
|
+
passRateTrendByDate.set(occurredDate, counts);
|
|
757
|
+
}
|
|
758
|
+
const passRateTrendRows = [...passRateTrendByDate.entries()]
|
|
759
|
+
.map(([date, counts]) => ({
|
|
760
|
+
date,
|
|
761
|
+
pass_rate: counts.total > 0 ? counts.triggered / counts.total : 0,
|
|
762
|
+
total_checks: counts.total,
|
|
763
|
+
}))
|
|
764
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
765
|
+
|
|
766
|
+
const pass_rate_trend = passRateTrendRows.map((row) => ({
|
|
767
|
+
date: row.date,
|
|
768
|
+
pass_rate: row.pass_rate,
|
|
769
|
+
total_checks: row.total_checks,
|
|
770
|
+
}));
|
|
771
|
+
|
|
772
|
+
// 2. Skill rankings — all skills with at least 1 check, ordered by pass rate
|
|
773
|
+
const skillRankingMap = new Map<string, { triggered_count: number; total_checks: number }>();
|
|
774
|
+
for (const row of trustedRows) {
|
|
775
|
+
const counts = skillRankingMap.get(row.skill_name) ?? { triggered_count: 0, total_checks: 0 };
|
|
776
|
+
counts.total_checks += 1;
|
|
777
|
+
if (row.triggered === 1) counts.triggered_count += 1;
|
|
778
|
+
skillRankingMap.set(row.skill_name, counts);
|
|
779
|
+
}
|
|
780
|
+
const skillRankingRows = [...skillRankingMap.entries()]
|
|
781
|
+
.map(([skill_name, counts]) => ({
|
|
782
|
+
skill_name,
|
|
783
|
+
pass_rate: counts.total_checks > 0 ? counts.triggered_count / counts.total_checks : 0,
|
|
784
|
+
total_checks: counts.total_checks,
|
|
785
|
+
triggered_count: counts.triggered_count,
|
|
786
|
+
}))
|
|
787
|
+
.sort(
|
|
788
|
+
(a, b) =>
|
|
789
|
+
b.pass_rate - a.pass_rate ||
|
|
790
|
+
b.total_checks - a.total_checks ||
|
|
791
|
+
a.skill_name.localeCompare(b.skill_name),
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const skill_rankings = skillRankingRows.map((row) => ({
|
|
289
795
|
skill_name: row.skill_name,
|
|
290
|
-
|
|
796
|
+
pass_rate: row.pass_rate,
|
|
291
797
|
total_checks: row.total_checks,
|
|
292
798
|
triggered_count: row.triggered_count,
|
|
293
|
-
pass_rate: row.total_checks > 0 ? row.triggered_count / row.total_checks : 0,
|
|
294
|
-
unique_sessions: row.unique_sessions,
|
|
295
|
-
last_seen: row.last_seen,
|
|
296
|
-
has_evidence: evidenceSkills.has(row.skill_name),
|
|
297
799
|
}));
|
|
800
|
+
|
|
801
|
+
// 3. Daily activity — last 84 days (12 weeks) for heatmap
|
|
802
|
+
const dailyActivityByDate = new Map<string, number>();
|
|
803
|
+
for (const row of trustedRows) {
|
|
804
|
+
const occurredDate = dateKey(row.occurred_at);
|
|
805
|
+
if (!occurredDate || occurredDate < cutoffDate(84)) continue;
|
|
806
|
+
dailyActivityByDate.set(occurredDate, (dailyActivityByDate.get(occurredDate) ?? 0) + 1);
|
|
807
|
+
}
|
|
808
|
+
const dailyActivityRows = [...dailyActivityByDate.entries()]
|
|
809
|
+
.map(([date, checks]) => ({ date, checks }))
|
|
810
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
811
|
+
|
|
812
|
+
const daily_activity = dailyActivityRows.map((row) => ({
|
|
813
|
+
date: row.date,
|
|
814
|
+
checks: row.checks,
|
|
815
|
+
}));
|
|
816
|
+
|
|
817
|
+
// 4. Evolution impact — before/after pass rates for deployed evolutions
|
|
818
|
+
const deployedRows = db
|
|
819
|
+
.query(
|
|
820
|
+
`SELECT ea.skill_name, ea.proposal_id, ea.timestamp as deployed_at
|
|
821
|
+
FROM evolution_audit ea
|
|
822
|
+
WHERE ea.action = 'deployed' AND ea.skill_name IS NOT NULL
|
|
823
|
+
ORDER BY ea.timestamp DESC`,
|
|
824
|
+
)
|
|
825
|
+
.all() as Array<{ skill_name: string; proposal_id: string; deployed_at: string }>;
|
|
826
|
+
|
|
827
|
+
const evolution_impact: AnalyticsResponse["evolution_impact"] = [];
|
|
828
|
+
for (const deploy of deployedRows) {
|
|
829
|
+
const beforeRows = trustedRows.filter(
|
|
830
|
+
(row) => row.skill_name === deploy.skill_name && (row.occurred_at ?? "") < deploy.deployed_at,
|
|
831
|
+
);
|
|
832
|
+
const afterRows = trustedRows.filter(
|
|
833
|
+
(row) =>
|
|
834
|
+
row.skill_name === deploy.skill_name && (row.occurred_at ?? "") >= deploy.deployed_at,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
evolution_impact.push({
|
|
838
|
+
skill_name: deploy.skill_name,
|
|
839
|
+
proposal_id: deploy.proposal_id,
|
|
840
|
+
deployed_at: deploy.deployed_at,
|
|
841
|
+
pass_rate_before:
|
|
842
|
+
beforeRows.length > 0
|
|
843
|
+
? beforeRows.filter((row) => row.triggered === 1).length / beforeRows.length
|
|
844
|
+
: 0,
|
|
845
|
+
pass_rate_after:
|
|
846
|
+
afterRows.length > 0
|
|
847
|
+
? afterRows.filter((row) => row.triggered === 1).length / afterRows.length
|
|
848
|
+
: 0,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// 5. Summary aggregates
|
|
853
|
+
const totalEvolutionsRow = db
|
|
854
|
+
.query(`SELECT COUNT(*) as c FROM evolution_audit WHERE action = 'deployed'`)
|
|
855
|
+
.get() as { c: number } | null;
|
|
856
|
+
|
|
857
|
+
const checks30dRows = trustedRows.filter((row) => {
|
|
858
|
+
const occurredDate = dateKey(row.occurred_at);
|
|
859
|
+
return occurredDate != null && occurredDate >= cutoffDate(30);
|
|
860
|
+
});
|
|
861
|
+
const activeSkills30d = new Set(checks30dRows.map((row) => row.skill_name));
|
|
862
|
+
|
|
863
|
+
// Average improvement across all deployed evolutions
|
|
864
|
+
let avgImprovement = 0;
|
|
865
|
+
if (evolution_impact.length > 0) {
|
|
866
|
+
const totalImprovement = evolution_impact.reduce(
|
|
867
|
+
(sum, e) => sum + (e.pass_rate_after - e.pass_rate_before),
|
|
868
|
+
0,
|
|
869
|
+
);
|
|
870
|
+
avgImprovement = totalImprovement / evolution_impact.length;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const summary: AnalyticsResponse["summary"] = {
|
|
874
|
+
total_evolutions: totalEvolutionsRow?.c ?? 0,
|
|
875
|
+
avg_improvement: avgImprovement,
|
|
876
|
+
total_checks_30d: checks30dRows.length,
|
|
877
|
+
active_skills: activeSkills30d.size,
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
pass_rate_trend,
|
|
882
|
+
skill_rankings,
|
|
883
|
+
daily_activity,
|
|
884
|
+
evolution_impact,
|
|
885
|
+
summary,
|
|
886
|
+
};
|
|
298
887
|
}
|
|
299
888
|
|
|
300
889
|
/**
|
|
@@ -390,13 +979,12 @@ export function getActiveSessionCount(db: Database): number {
|
|
|
390
979
|
* session is still in progress (no session_telemetry row yet).
|
|
391
980
|
*/
|
|
392
981
|
export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[] {
|
|
982
|
+
// Step 1: Fetch recent invocations without JOIN (avoids materializing full JOIN before LIMIT)
|
|
393
983
|
const rows = db
|
|
394
984
|
.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
|
|
985
|
+
`SELECT occurred_at, session_id, skill_name, query, triggered
|
|
986
|
+
FROM skill_invocations
|
|
987
|
+
ORDER BY occurred_at DESC
|
|
400
988
|
LIMIT ?`,
|
|
401
989
|
)
|
|
402
990
|
.all(limit) as Array<{
|
|
@@ -405,16 +993,27 @@ export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[
|
|
|
405
993
|
skill_name: string;
|
|
406
994
|
query: string;
|
|
407
995
|
triggered: number;
|
|
408
|
-
is_live: number;
|
|
409
996
|
}>;
|
|
410
997
|
|
|
998
|
+
if (rows.length === 0) return [];
|
|
999
|
+
|
|
1000
|
+
// Step 2: Batch lookup which sessions have completed (have a telemetry row)
|
|
1001
|
+
const uniqueSessionIds = [...new Set(rows.map((r) => r.session_id))];
|
|
1002
|
+
const placeholders = uniqueSessionIds.map(() => "?").join(",");
|
|
1003
|
+
const completedRows = db
|
|
1004
|
+
.query(
|
|
1005
|
+
`SELECT DISTINCT session_id FROM session_telemetry WHERE session_id IN (${placeholders})`,
|
|
1006
|
+
)
|
|
1007
|
+
.all(...uniqueSessionIds) as Array<{ session_id: string }>;
|
|
1008
|
+
const completedSessions = new Set(completedRows.map((r) => r.session_id));
|
|
1009
|
+
|
|
411
1010
|
return rows.map((row) => ({
|
|
412
1011
|
timestamp: row.occurred_at,
|
|
413
1012
|
session_id: row.session_id,
|
|
414
1013
|
skill_name: row.skill_name,
|
|
415
1014
|
query: row.query ?? "",
|
|
416
1015
|
triggered: row.triggered === 1,
|
|
417
|
-
is_live: row.
|
|
1016
|
+
is_live: !completedSessions.has(row.session_id),
|
|
418
1017
|
}));
|
|
419
1018
|
}
|
|
420
1019
|
|
|
@@ -688,6 +1287,72 @@ export function queryGradingResults(db: Database): Array<{
|
|
|
688
1287
|
}>;
|
|
689
1288
|
}
|
|
690
1289
|
|
|
1290
|
+
export function getCreatorContributionStagingCounts(db: Database): Array<{
|
|
1291
|
+
skill_name: string;
|
|
1292
|
+
pending_count: number;
|
|
1293
|
+
}> {
|
|
1294
|
+
return db
|
|
1295
|
+
.query(
|
|
1296
|
+
`SELECT skill_name, COUNT(*) AS pending_count
|
|
1297
|
+
FROM creator_contribution_staging
|
|
1298
|
+
WHERE status = 'pending'
|
|
1299
|
+
GROUP BY skill_name
|
|
1300
|
+
ORDER BY skill_name`,
|
|
1301
|
+
)
|
|
1302
|
+
.all() as Array<{
|
|
1303
|
+
skill_name: string;
|
|
1304
|
+
pending_count: number;
|
|
1305
|
+
}>;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export interface CreatorContributionRelayStats {
|
|
1309
|
+
pending: number;
|
|
1310
|
+
sending: number;
|
|
1311
|
+
sent: number;
|
|
1312
|
+
failed: number;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
export interface CreatorContributionStagingRow {
|
|
1316
|
+
id: number;
|
|
1317
|
+
dedupe_key: string;
|
|
1318
|
+
skill_name: string;
|
|
1319
|
+
creator_id: string;
|
|
1320
|
+
payload_json: string;
|
|
1321
|
+
status: string;
|
|
1322
|
+
staged_at: string;
|
|
1323
|
+
updated_at: string;
|
|
1324
|
+
last_error: string | null;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export function getCreatorContributionRelayStats(db: Database): CreatorContributionRelayStats {
|
|
1328
|
+
const row = db
|
|
1329
|
+
.query(
|
|
1330
|
+
`SELECT
|
|
1331
|
+
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending,
|
|
1332
|
+
COALESCE(SUM(CASE WHEN status = 'sending' THEN 1 ELSE 0 END), 0) AS sending,
|
|
1333
|
+
COALESCE(SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), 0) AS sent,
|
|
1334
|
+
COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) AS failed
|
|
1335
|
+
FROM creator_contribution_staging`,
|
|
1336
|
+
)
|
|
1337
|
+
.get() as CreatorContributionRelayStats | null;
|
|
1338
|
+
return row ?? { pending: 0, sending: 0, sent: 0, failed: 0 };
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export function getPendingCreatorContributionRows(
|
|
1342
|
+
db: Database,
|
|
1343
|
+
limit = 50,
|
|
1344
|
+
): CreatorContributionStagingRow[] {
|
|
1345
|
+
return db
|
|
1346
|
+
.query(
|
|
1347
|
+
`SELECT id, dedupe_key, skill_name, creator_id, payload_json, status, staged_at, updated_at, last_error
|
|
1348
|
+
FROM creator_contribution_staging
|
|
1349
|
+
WHERE status = 'pending'
|
|
1350
|
+
ORDER BY id ASC
|
|
1351
|
+
LIMIT ?`,
|
|
1352
|
+
)
|
|
1353
|
+
.all(limit) as CreatorContributionStagingRow[];
|
|
1354
|
+
}
|
|
1355
|
+
|
|
691
1356
|
// -- Canonical record staging query -------------------------------------------
|
|
692
1357
|
|
|
693
1358
|
/**
|
|
@@ -914,6 +1579,557 @@ export function getOldestPendingAge(db: Database): number | null {
|
|
|
914
1579
|
}
|
|
915
1580
|
}
|
|
916
1581
|
|
|
1582
|
+
// -- Execution metrics & commit tracking queries ------------------------------
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Aggregate execution_facts enrichment columns for a set of session IDs.
|
|
1586
|
+
*
|
|
1587
|
+
* Returns file change stats, cost totals, token breakdowns, artifact counts,
|
|
1588
|
+
* and session_type distribution across the provided sessions.
|
|
1589
|
+
*/
|
|
1590
|
+
export function getExecutionMetrics(db: Database, sessionIds: string[]): ExecutionMetrics {
|
|
1591
|
+
const empty: ExecutionMetrics = {
|
|
1592
|
+
avg_files_changed: 0,
|
|
1593
|
+
total_lines_added: 0,
|
|
1594
|
+
total_lines_removed: 0,
|
|
1595
|
+
total_cost_usd: 0,
|
|
1596
|
+
avg_cost_usd: 0,
|
|
1597
|
+
cached_input_tokens_total: 0,
|
|
1598
|
+
reasoning_output_tokens_total: 0,
|
|
1599
|
+
artifact_count: 0,
|
|
1600
|
+
session_type_distribution: {},
|
|
1601
|
+
};
|
|
1602
|
+
if (sessionIds.length === 0) return empty;
|
|
1603
|
+
|
|
1604
|
+
const placeholders = sessionIds.map(() => "?").join(",");
|
|
1605
|
+
const row = db
|
|
1606
|
+
.query(
|
|
1607
|
+
`SELECT
|
|
1608
|
+
COALESCE(AVG(files_changed), 0) AS avg_files_changed,
|
|
1609
|
+
COALESCE(SUM(lines_added), 0) AS total_lines_added,
|
|
1610
|
+
COALESCE(SUM(lines_removed), 0) AS total_lines_removed,
|
|
1611
|
+
COALESCE(SUM(cost_usd), 0) AS total_cost_usd,
|
|
1612
|
+
COALESCE(AVG(cost_usd), 0) AS avg_cost_usd,
|
|
1613
|
+
COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens_total,
|
|
1614
|
+
COALESCE(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens_total,
|
|
1615
|
+
COALESCE(SUM(artifact_count), 0) AS artifact_count
|
|
1616
|
+
FROM execution_facts
|
|
1617
|
+
WHERE session_id IN (${placeholders})`,
|
|
1618
|
+
)
|
|
1619
|
+
.get(...sessionIds) as {
|
|
1620
|
+
avg_files_changed: number;
|
|
1621
|
+
total_lines_added: number;
|
|
1622
|
+
total_lines_removed: number;
|
|
1623
|
+
total_cost_usd: number;
|
|
1624
|
+
avg_cost_usd: number;
|
|
1625
|
+
cached_input_tokens_total: number;
|
|
1626
|
+
reasoning_output_tokens_total: number;
|
|
1627
|
+
artifact_count: number;
|
|
1628
|
+
} | null;
|
|
1629
|
+
|
|
1630
|
+
// Session type distribution
|
|
1631
|
+
const typeRows = db
|
|
1632
|
+
.query(
|
|
1633
|
+
`SELECT session_type, COUNT(*) AS count
|
|
1634
|
+
FROM execution_facts
|
|
1635
|
+
WHERE session_id IN (${placeholders}) AND session_type IS NOT NULL
|
|
1636
|
+
GROUP BY session_type`,
|
|
1637
|
+
)
|
|
1638
|
+
.all(...sessionIds) as Array<{ session_type: string; count: number }>;
|
|
1639
|
+
|
|
1640
|
+
const session_type_distribution: Record<string, number> = {};
|
|
1641
|
+
for (const tr of typeRows) {
|
|
1642
|
+
session_type_distribution[tr.session_type] = tr.count;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return {
|
|
1646
|
+
avg_files_changed: row?.avg_files_changed ?? 0,
|
|
1647
|
+
total_lines_added: row?.total_lines_added ?? 0,
|
|
1648
|
+
total_lines_removed: row?.total_lines_removed ?? 0,
|
|
1649
|
+
total_cost_usd: row?.total_cost_usd ?? 0,
|
|
1650
|
+
avg_cost_usd: row?.avg_cost_usd ?? 0,
|
|
1651
|
+
cached_input_tokens_total: row?.cached_input_tokens_total ?? 0,
|
|
1652
|
+
reasoning_output_tokens_total: row?.reasoning_output_tokens_total ?? 0,
|
|
1653
|
+
artifact_count: row?.artifact_count ?? 0,
|
|
1654
|
+
session_type_distribution,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Get all commits tracked for a given session.
|
|
1660
|
+
*/
|
|
1661
|
+
export function getSessionCommits(db: Database, sessionId: string): CommitRecord[] {
|
|
1662
|
+
return db
|
|
1663
|
+
.query(
|
|
1664
|
+
`SELECT commit_sha, commit_title, branch, repo_remote, timestamp
|
|
1665
|
+
FROM commit_tracking
|
|
1666
|
+
WHERE session_id = ?
|
|
1667
|
+
ORDER BY timestamp DESC`,
|
|
1668
|
+
)
|
|
1669
|
+
.all(sessionId) as CommitRecord[];
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Aggregate commit stats for a skill by joining commit_tracking to skill_invocations
|
|
1674
|
+
* via shared session_id.
|
|
1675
|
+
*/
|
|
1676
|
+
export function getSkillCommitSummary(db: Database, skillName: string): CommitSummary {
|
|
1677
|
+
const empty: CommitSummary = {
|
|
1678
|
+
total_commits: 0,
|
|
1679
|
+
unique_branches: 0,
|
|
1680
|
+
recent_commits: [],
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
const statsRow = db
|
|
1684
|
+
.query(
|
|
1685
|
+
`WITH skill_sessions AS (
|
|
1686
|
+
SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
|
|
1687
|
+
)
|
|
1688
|
+
SELECT
|
|
1689
|
+
COUNT(*) AS total_commits,
|
|
1690
|
+
COUNT(DISTINCT ct.branch) AS unique_branches
|
|
1691
|
+
FROM commit_tracking ct
|
|
1692
|
+
WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)`,
|
|
1693
|
+
)
|
|
1694
|
+
.get(skillName) as { total_commits: number; unique_branches: number } | null;
|
|
1695
|
+
|
|
1696
|
+
if (!statsRow || statsRow.total_commits === 0) return empty;
|
|
1697
|
+
|
|
1698
|
+
const recentRows = db
|
|
1699
|
+
.query(
|
|
1700
|
+
`WITH skill_sessions AS (
|
|
1701
|
+
SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
|
|
1702
|
+
)
|
|
1703
|
+
SELECT ct.commit_sha, ct.commit_title, ct.branch, ct.timestamp
|
|
1704
|
+
FROM commit_tracking ct
|
|
1705
|
+
WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)
|
|
1706
|
+
ORDER BY ct.timestamp DESC
|
|
1707
|
+
LIMIT 20`,
|
|
1708
|
+
)
|
|
1709
|
+
.all(skillName) as Array<{
|
|
1710
|
+
commit_sha: string;
|
|
1711
|
+
commit_title: string | null;
|
|
1712
|
+
branch: string | null;
|
|
1713
|
+
timestamp: string;
|
|
1714
|
+
}>;
|
|
1715
|
+
|
|
1716
|
+
return {
|
|
1717
|
+
total_commits: statsRow.total_commits,
|
|
1718
|
+
unique_branches: statsRow.unique_branches,
|
|
1719
|
+
recent_commits: recentRows.map((r) => ({
|
|
1720
|
+
sha: r.commit_sha,
|
|
1721
|
+
title: r.commit_title ?? "",
|
|
1722
|
+
branch: r.branch ?? "",
|
|
1723
|
+
timestamp: r.timestamp,
|
|
1724
|
+
})),
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// -- Helpers ------------------------------------------------------------------
|
|
1729
|
+
|
|
1730
|
+
// -- Autonomy-first dashboard queries -----------------------------------------
|
|
1731
|
+
|
|
1732
|
+
export interface SkillTrustSummary {
|
|
1733
|
+
skill_name: string;
|
|
1734
|
+
total_checks: number;
|
|
1735
|
+
triggered_count: number;
|
|
1736
|
+
miss_rate: number;
|
|
1737
|
+
system_like_count: number;
|
|
1738
|
+
system_like_rate: number;
|
|
1739
|
+
prompt_link_rate: number;
|
|
1740
|
+
latest_action: string | null;
|
|
1741
|
+
pass_rate: number;
|
|
1742
|
+
last_seen: string | null;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export interface TrustedSkillObservationRow {
|
|
1746
|
+
skill_name: string;
|
|
1747
|
+
session_id: string;
|
|
1748
|
+
occurred_at: string | null;
|
|
1749
|
+
triggered: number;
|
|
1750
|
+
matched_prompt_id: string | null;
|
|
1751
|
+
confidence: number | null;
|
|
1752
|
+
invocation_mode: string | null;
|
|
1753
|
+
query_text: string;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
export function queryTrustedSkillObservationRows(db: Database): TrustedSkillObservationRow[] {
|
|
1757
|
+
const SYSTEM_LIKE_PREFIXES = ["<system_instruction>", "<system-instruction>", "<command-name>"];
|
|
1758
|
+
const INTERNAL_EVAL_MARKERS = [
|
|
1759
|
+
"you are an evaluation assistant",
|
|
1760
|
+
"you are a skill description optimizer",
|
|
1761
|
+
"would each query trigger this skill",
|
|
1762
|
+
"propose an improved description",
|
|
1763
|
+
"failure patterns:",
|
|
1764
|
+
"output only valid json",
|
|
1765
|
+
];
|
|
1766
|
+
const isSystemLike = (text: string | null | undefined): boolean => {
|
|
1767
|
+
if (!text) return false;
|
|
1768
|
+
const trimmed = text.trimStart();
|
|
1769
|
+
return SYSTEM_LIKE_PREFIXES.some((p) => trimmed.startsWith(p));
|
|
1770
|
+
};
|
|
1771
|
+
const isInternalSelftunePrompt = (
|
|
1772
|
+
text: string | null | undefined,
|
|
1773
|
+
promptKind: string | null | undefined,
|
|
1774
|
+
): boolean => {
|
|
1775
|
+
if (!text) return false;
|
|
1776
|
+
const lowered = text.toLowerCase();
|
|
1777
|
+
return (
|
|
1778
|
+
promptKind === "meta" && INTERNAL_EVAL_MARKERS.some((marker) => lowered.includes(marker))
|
|
1779
|
+
);
|
|
1780
|
+
};
|
|
1781
|
+
const isPollutingPrompt = (
|
|
1782
|
+
text: string | null | undefined,
|
|
1783
|
+
promptKind: string | null | undefined,
|
|
1784
|
+
): boolean => isSystemLike(text) || isInternalSelftunePrompt(text, promptKind);
|
|
1785
|
+
const classifyObservationKind = (
|
|
1786
|
+
skillInvocationId: string,
|
|
1787
|
+
captureMode: string | null,
|
|
1788
|
+
triggered: number,
|
|
1789
|
+
rawSourceRefJson: string | null,
|
|
1790
|
+
): "canonical" | "repaired_trigger" | "repaired_contextual_miss" | "legacy_materialized" => {
|
|
1791
|
+
if (skillInvocationId.includes(":su:")) return "legacy_materialized";
|
|
1792
|
+
if (captureMode === "repair") {
|
|
1793
|
+
const rawSourceRef = safeParseJson(rawSourceRefJson) as {
|
|
1794
|
+
metadata?: { miss_type?: string };
|
|
1795
|
+
} | null;
|
|
1796
|
+
if (triggered === 0 && rawSourceRef?.metadata?.miss_type === "contextual_read") {
|
|
1797
|
+
return "repaired_contextual_miss";
|
|
1798
|
+
}
|
|
1799
|
+
return "repaired_trigger";
|
|
1800
|
+
}
|
|
1801
|
+
return "canonical";
|
|
1802
|
+
};
|
|
1803
|
+
const normalizeQueryForGrouping = (query: string) =>
|
|
1804
|
+
query.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1805
|
+
|
|
1806
|
+
const rows = db
|
|
1807
|
+
.query(
|
|
1808
|
+
`SELECT
|
|
1809
|
+
si.skill_name,
|
|
1810
|
+
si.session_id,
|
|
1811
|
+
si.occurred_at,
|
|
1812
|
+
si.triggered,
|
|
1813
|
+
si.matched_prompt_id,
|
|
1814
|
+
si.confidence,
|
|
1815
|
+
si.invocation_mode,
|
|
1816
|
+
si.skill_invocation_id,
|
|
1817
|
+
si.capture_mode,
|
|
1818
|
+
si.raw_source_ref,
|
|
1819
|
+
si.query,
|
|
1820
|
+
p.prompt_text,
|
|
1821
|
+
p.prompt_kind
|
|
1822
|
+
FROM skill_invocations si
|
|
1823
|
+
LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id`,
|
|
1824
|
+
)
|
|
1825
|
+
.all() as Array<{
|
|
1826
|
+
skill_name: string;
|
|
1827
|
+
session_id: string;
|
|
1828
|
+
occurred_at: string | null;
|
|
1829
|
+
triggered: number;
|
|
1830
|
+
matched_prompt_id: string | null;
|
|
1831
|
+
confidence: number | null;
|
|
1832
|
+
invocation_mode: string | null;
|
|
1833
|
+
skill_invocation_id: string;
|
|
1834
|
+
capture_mode: string | null;
|
|
1835
|
+
raw_source_ref: string | null;
|
|
1836
|
+
query: string | null;
|
|
1837
|
+
prompt_text: string | null;
|
|
1838
|
+
prompt_kind: string | null;
|
|
1839
|
+
}>;
|
|
1840
|
+
|
|
1841
|
+
const bySkill = new Map<
|
|
1842
|
+
string,
|
|
1843
|
+
Array<{
|
|
1844
|
+
skill_name: string;
|
|
1845
|
+
session_id: string;
|
|
1846
|
+
occurred_at: string | null;
|
|
1847
|
+
triggered: number;
|
|
1848
|
+
matched_prompt_id: string | null;
|
|
1849
|
+
confidence: number | null;
|
|
1850
|
+
invocation_mode: string | null;
|
|
1851
|
+
queryText: string;
|
|
1852
|
+
isPolluting: boolean;
|
|
1853
|
+
observation_kind:
|
|
1854
|
+
| "canonical"
|
|
1855
|
+
| "repaired_trigger"
|
|
1856
|
+
| "repaired_contextual_miss"
|
|
1857
|
+
| "legacy_materialized";
|
|
1858
|
+
groupKey: string;
|
|
1859
|
+
}>
|
|
1860
|
+
>();
|
|
1861
|
+
const trustedRows: Array<{
|
|
1862
|
+
skill_name: string;
|
|
1863
|
+
session_id: string;
|
|
1864
|
+
occurred_at: string | null;
|
|
1865
|
+
triggered: number;
|
|
1866
|
+
matched_prompt_id: string | null;
|
|
1867
|
+
confidence: number | null;
|
|
1868
|
+
invocation_mode: string | null;
|
|
1869
|
+
query_text: string;
|
|
1870
|
+
}> = [];
|
|
1871
|
+
|
|
1872
|
+
for (const row of rows) {
|
|
1873
|
+
const queryText = row.query || row.prompt_text || "";
|
|
1874
|
+
const pollutionText = row.prompt_text || row.query || "";
|
|
1875
|
+
const observation_kind = classifyObservationKind(
|
|
1876
|
+
row.skill_invocation_id,
|
|
1877
|
+
row.capture_mode,
|
|
1878
|
+
row.triggered,
|
|
1879
|
+
row.raw_source_ref,
|
|
1880
|
+
);
|
|
1881
|
+
if (isPollutingPrompt(pollutionText, row.prompt_kind)) continue;
|
|
1882
|
+
if (observation_kind === "legacy_materialized") continue;
|
|
1883
|
+
|
|
1884
|
+
const normalizedQuery = normalizeQueryForGrouping(queryText);
|
|
1885
|
+
const groupKey =
|
|
1886
|
+
normalizedQuery.length > 0
|
|
1887
|
+
? `${row.session_id}::${normalizedQuery}`
|
|
1888
|
+
: `${row.skill_invocation_id}`;
|
|
1889
|
+
const arr = bySkill.get(row.skill_name);
|
|
1890
|
+
const enriched = {
|
|
1891
|
+
skill_name: row.skill_name,
|
|
1892
|
+
session_id: row.session_id,
|
|
1893
|
+
occurred_at: row.occurred_at,
|
|
1894
|
+
triggered: row.triggered,
|
|
1895
|
+
matched_prompt_id: row.matched_prompt_id,
|
|
1896
|
+
confidence: row.confidence,
|
|
1897
|
+
invocation_mode: row.invocation_mode,
|
|
1898
|
+
queryText,
|
|
1899
|
+
isPolluting: false,
|
|
1900
|
+
observation_kind,
|
|
1901
|
+
groupKey,
|
|
1902
|
+
};
|
|
1903
|
+
if (arr) arr.push(enriched);
|
|
1904
|
+
else bySkill.set(row.skill_name, [enriched]);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
for (const [, skillRows] of bySkill.entries()) {
|
|
1908
|
+
const grouped = new Map<string, typeof skillRows>();
|
|
1909
|
+
for (const row of skillRows) {
|
|
1910
|
+
const arr = grouped.get(row.groupKey);
|
|
1911
|
+
if (arr) arr.push(row);
|
|
1912
|
+
else grouped.set(row.groupKey, [row]);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const deduped = [...grouped.values()].map((group) => {
|
|
1916
|
+
const sorted = [...group].sort((a, b) => {
|
|
1917
|
+
const aScore =
|
|
1918
|
+
(a.triggered === 1 ? 100 : 0) +
|
|
1919
|
+
(a.observation_kind === "canonical" ? 20 : 0) +
|
|
1920
|
+
(a.observation_kind === "repaired_trigger" ? 15 : 0);
|
|
1921
|
+
const bScore =
|
|
1922
|
+
(b.triggered === 1 ? 100 : 0) +
|
|
1923
|
+
(b.observation_kind === "canonical" ? 20 : 0) +
|
|
1924
|
+
(b.observation_kind === "repaired_trigger" ? 15 : 0);
|
|
1925
|
+
if (aScore !== bScore) return bScore - aScore;
|
|
1926
|
+
return (b.occurred_at ?? "").localeCompare(a.occurred_at ?? "");
|
|
1927
|
+
});
|
|
1928
|
+
return sorted[0]!;
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
trustedRows.push(
|
|
1932
|
+
...deduped.map((row) => ({
|
|
1933
|
+
skill_name: row.skill_name,
|
|
1934
|
+
session_id: row.session_id,
|
|
1935
|
+
occurred_at: row.occurred_at,
|
|
1936
|
+
triggered: row.triggered,
|
|
1937
|
+
matched_prompt_id: row.matched_prompt_id,
|
|
1938
|
+
confidence: row.confidence,
|
|
1939
|
+
invocation_mode: row.invocation_mode,
|
|
1940
|
+
query_text: row.queryText,
|
|
1941
|
+
})),
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return trustedRows;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
export function getSkillTrustSummaries(db: Database): SkillTrustSummary[] {
|
|
1949
|
+
const rows = queryTrustedSkillObservationRows(db);
|
|
1950
|
+
|
|
1951
|
+
// Build latest_action map from evolution_audit
|
|
1952
|
+
const auditRows = db
|
|
1953
|
+
.query(
|
|
1954
|
+
`SELECT skill_name, action, timestamp
|
|
1955
|
+
FROM evolution_audit
|
|
1956
|
+
WHERE skill_name IS NOT NULL
|
|
1957
|
+
ORDER BY timestamp DESC`,
|
|
1958
|
+
)
|
|
1959
|
+
.all() as Array<{
|
|
1960
|
+
skill_name: string | null;
|
|
1961
|
+
action: string;
|
|
1962
|
+
timestamp: string;
|
|
1963
|
+
}>;
|
|
1964
|
+
|
|
1965
|
+
const latestActions = new Map<string, string>();
|
|
1966
|
+
for (const row of auditRows) {
|
|
1967
|
+
if (row.skill_name && !latestActions.has(row.skill_name)) {
|
|
1968
|
+
latestActions.set(row.skill_name, row.action);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
const rowsBySkill = new Map<string, typeof rows>();
|
|
1973
|
+
for (const row of rows) {
|
|
1974
|
+
const arr = rowsBySkill.get(row.skill_name);
|
|
1975
|
+
if (arr) arr.push(row);
|
|
1976
|
+
else rowsBySkill.set(row.skill_name, [row]);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const summaries: SkillTrustSummary[] = [];
|
|
1980
|
+
for (const [skillName, skillRows] of rowsBySkill.entries()) {
|
|
1981
|
+
const total = skillRows.length;
|
|
1982
|
+
const triggered = skillRows.filter((row) => row.triggered === 1).length;
|
|
1983
|
+
const promptLinked = skillRows.filter((row) => row.matched_prompt_id != null).length;
|
|
1984
|
+
const lastSeen =
|
|
1985
|
+
skillRows
|
|
1986
|
+
.map((row) => row.occurred_at)
|
|
1987
|
+
.filter((value): value is string => value != null)
|
|
1988
|
+
.sort((a, b) => b.localeCompare(a))[0] ?? null;
|
|
1989
|
+
|
|
1990
|
+
summaries.push({
|
|
1991
|
+
skill_name: skillName,
|
|
1992
|
+
total_checks: total,
|
|
1993
|
+
triggered_count: triggered,
|
|
1994
|
+
miss_rate: total > 0 ? (total - triggered) / total : 0,
|
|
1995
|
+
system_like_count: 0,
|
|
1996
|
+
system_like_rate: 0,
|
|
1997
|
+
prompt_link_rate: total > 0 ? promptLinked / total : 0,
|
|
1998
|
+
latest_action: latestActions.get(skillName) ?? null,
|
|
1999
|
+
pass_rate: total > 0 ? triggered / total : 0,
|
|
2000
|
+
last_seen: lastSeen,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return summaries;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
export function getAttentionQueue(db: Database): AttentionItem[] {
|
|
2008
|
+
const summaries = getSkillTrustSummaries(db);
|
|
2009
|
+
const pending = getPendingProposals(db);
|
|
2010
|
+
const pendingSkills = new Set(pending.map((p) => p.skill_name).filter(Boolean));
|
|
2011
|
+
|
|
2012
|
+
const items: AttentionItem[] = [];
|
|
2013
|
+
|
|
2014
|
+
for (const s of summaries) {
|
|
2015
|
+
if (s.latest_action === "rolled_back") {
|
|
2016
|
+
items.push({
|
|
2017
|
+
skill_name: s.skill_name,
|
|
2018
|
+
category: "needs_review",
|
|
2019
|
+
severity: "critical",
|
|
2020
|
+
reason: "Rolled back after deployment",
|
|
2021
|
+
recommended_action: "Review rollback evidence and decide whether to re-evolve",
|
|
2022
|
+
timestamp: s.last_seen ?? "",
|
|
2023
|
+
});
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (pendingSkills.has(s.skill_name)) {
|
|
2028
|
+
items.push({
|
|
2029
|
+
skill_name: s.skill_name,
|
|
2030
|
+
category: "needs_review",
|
|
2031
|
+
severity: "info",
|
|
2032
|
+
reason: "Proposal awaiting review",
|
|
2033
|
+
recommended_action: "Review and approve or reject the pending proposal",
|
|
2034
|
+
timestamp: s.last_seen ?? "",
|
|
2035
|
+
});
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (s.total_checks < 5) continue;
|
|
2040
|
+
|
|
2041
|
+
if (s.miss_rate > 0.1) {
|
|
2042
|
+
items.push({
|
|
2043
|
+
skill_name: s.skill_name,
|
|
2044
|
+
category: "regression",
|
|
2045
|
+
severity: "warning",
|
|
2046
|
+
reason: `High miss rate (${Math.round(s.miss_rate * 100)}%)`,
|
|
2047
|
+
recommended_action: "Review missed invocations and consider evolving the skill description",
|
|
2048
|
+
timestamp: s.last_seen ?? "",
|
|
2049
|
+
});
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (s.system_like_rate > 0.1) {
|
|
2054
|
+
items.push({
|
|
2055
|
+
skill_name: s.skill_name,
|
|
2056
|
+
category: "polluted",
|
|
2057
|
+
severity: "warning",
|
|
2058
|
+
reason: `Possible telemetry pollution (${Math.round(s.system_like_rate * 100)}% system-like)`,
|
|
2059
|
+
recommended_action: "Inspect prompts for system-injected noise",
|
|
2060
|
+
timestamp: s.last_seen ?? "",
|
|
2061
|
+
});
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
return items;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
export function getRecentDecisions(db: Database, limit = 20): AutonomousDecision[] {
|
|
2070
|
+
const rows = db
|
|
2071
|
+
.query(
|
|
2072
|
+
`SELECT timestamp, proposal_id, skill_name, action, details, eval_snapshot_json
|
|
2073
|
+
FROM evolution_audit
|
|
2074
|
+
WHERE timestamp >= datetime('now', '-7 days')
|
|
2075
|
+
ORDER BY timestamp DESC
|
|
2076
|
+
LIMIT ?`,
|
|
2077
|
+
)
|
|
2078
|
+
.all(limit) as Array<{
|
|
2079
|
+
timestamp: string;
|
|
2080
|
+
proposal_id: string;
|
|
2081
|
+
skill_name: string | null;
|
|
2082
|
+
action: string;
|
|
2083
|
+
details: string;
|
|
2084
|
+
eval_snapshot_json: string | null;
|
|
2085
|
+
}>;
|
|
2086
|
+
|
|
2087
|
+
return rows
|
|
2088
|
+
.filter((row) => row.skill_name != null)
|
|
2089
|
+
.flatMap((row) => {
|
|
2090
|
+
const evalSnapshot = safeParseJson(row.eval_snapshot_json) as {
|
|
2091
|
+
regressions?: unknown[];
|
|
2092
|
+
} | null;
|
|
2093
|
+
|
|
2094
|
+
let kind: DecisionKind | null;
|
|
2095
|
+
switch (row.action) {
|
|
2096
|
+
case "proposed":
|
|
2097
|
+
case "created":
|
|
2098
|
+
kind = "proposal_created";
|
|
2099
|
+
break;
|
|
2100
|
+
case "rejected":
|
|
2101
|
+
kind = "proposal_rejected";
|
|
2102
|
+
break;
|
|
2103
|
+
case "validated":
|
|
2104
|
+
kind =
|
|
2105
|
+
evalSnapshot?.regressions && evalSnapshot.regressions.length > 0
|
|
2106
|
+
? "validation_failed"
|
|
2107
|
+
: "proposal_created"; // validated without regressions is still a creation step
|
|
2108
|
+
break;
|
|
2109
|
+
case "deployed":
|
|
2110
|
+
kind = "proposal_deployed";
|
|
2111
|
+
break;
|
|
2112
|
+
case "rolled_back":
|
|
2113
|
+
kind = "rollback_triggered";
|
|
2114
|
+
break;
|
|
2115
|
+
default:
|
|
2116
|
+
kind = null;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (!kind) return [];
|
|
2120
|
+
|
|
2121
|
+
return [
|
|
2122
|
+
{
|
|
2123
|
+
timestamp: row.timestamp,
|
|
2124
|
+
kind,
|
|
2125
|
+
skill_name: row.skill_name!,
|
|
2126
|
+
proposal_id: row.proposal_id,
|
|
2127
|
+
summary: row.details ?? "",
|
|
2128
|
+
},
|
|
2129
|
+
];
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
917
2133
|
// -- Helpers ------------------------------------------------------------------
|
|
918
2134
|
|
|
919
2135
|
export function safeParseJsonArray<T = string>(json: string | null): T[] {
|