context-mode 1.0.99 → 1.0.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +22 -8
- package/build/adapters/claude-code/hooks.d.ts +4 -2
- package/build/adapters/claude-code/hooks.js +11 -4
- package/build/adapters/codex/index.d.ts +1 -1
- package/build/adapters/codex/index.js +6 -5
- package/build/adapters/gemini-cli/hooks.js +2 -1
- package/build/adapters/jetbrains-copilot/hooks.js +2 -1
- package/build/adapters/kiro/hooks.js +2 -1
- package/build/adapters/qwen-code/index.d.ts +1 -1
- package/build/adapters/qwen-code/index.js +13 -10
- package/build/adapters/types.d.ts +13 -0
- package/build/adapters/types.js +23 -1
- package/build/adapters/vscode-copilot/hooks.js +2 -1
- package/build/openclaw-plugin.js +3 -2
- package/build/pi-extension.js +1 -1
- package/build/search/auto-memory.d.ts +29 -0
- package/build/search/auto-memory.js +121 -0
- package/build/search/unified.d.ts +41 -0
- package/build/search/unified.js +89 -0
- package/build/server.js +73 -24
- package/build/session/analytics.js +1 -1
- package/build/session/db.d.ts +17 -0
- package/build/session/db.js +28 -0
- package/build/session/extract.d.ts +4 -0
- package/build/session/extract.js +232 -1
- package/build/session/snapshot.js +31 -0
- package/build/store.js +67 -4
- package/build/types.d.ts +1 -0
- package/cli.bundle.mjs +254 -119
- package/configs/claude-code/CLAUDE.md +21 -1
- package/configs/codex/AGENTS.md +22 -1
- package/configs/cursor/context-mode.mdc +18 -1
- package/configs/gemini-cli/GEMINI.md +22 -1
- package/configs/jetbrains-copilot/copilot-instructions.md +22 -1
- package/configs/kilo/AGENTS.md +19 -2
- package/configs/kiro/KIRO.md +18 -1
- package/configs/openclaw/AGENTS.md +22 -2
- package/configs/opencode/AGENTS.md +18 -1
- package/configs/pi/AGENTS.md +18 -1
- package/configs/qwen-code/QWEN.md +38 -18
- package/configs/vscode-copilot/copilot-instructions.md +22 -1
- package/hooks/auto-injection.mjs +76 -0
- package/hooks/codex/userpromptsubmit.mjs +1 -1
- package/hooks/core/mcp-ready.mjs +7 -1
- package/hooks/ensure-deps.mjs +35 -7
- package/hooks/posttooluse.mjs +50 -1
- package/hooks/precompact.mjs +9 -0
- package/hooks/pretooluse.mjs +27 -0
- package/hooks/routing-block.mjs +7 -1
- package/hooks/session-db.bundle.mjs +19 -13
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-snapshot.bundle.mjs +18 -17
- package/hooks/sessionstart.mjs +17 -0
- package/hooks/userpromptsubmit.mjs +1 -1
- package/insight/server.mjs +379 -1
- package/insight/src/lib/api.ts +88 -16
- package/insight/src/routes/index.tsx +566 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +222 -87
- package/start.mjs +3 -1
package/insight/server.mjs
CHANGED
|
@@ -52,6 +52,24 @@ const SESSION_DIR = process.env.INSIGHT_SESSION_DIR || join(homedir(), ".claude"
|
|
|
52
52
|
const CONTENT_DIR = process.env.INSIGHT_CONTENT_DIR || join(homedir(), ".claude", "context-mode", "content");
|
|
53
53
|
const DIST_DIR = join(__dirname, "dist");
|
|
54
54
|
|
|
55
|
+
// ── Response cache (5min TTL) ────────────────────────────
|
|
56
|
+
// Prevents double DB open when dashboard loads /analytics + /category-analytics
|
|
57
|
+
const _cache = new Map();
|
|
58
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
59
|
+
function cached(key, fn) {
|
|
60
|
+
const entry = _cache.get(key);
|
|
61
|
+
if (entry && Date.now() - entry.ts < CACHE_TTL_MS) return entry.data;
|
|
62
|
+
try {
|
|
63
|
+
const data = fn();
|
|
64
|
+
_cache.set(key, { data, ts: Date.now() });
|
|
65
|
+
return data;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// Cache the error for 30s to avoid repeated expensive failures
|
|
68
|
+
_cache.set(key, { data: { error: String(e) }, ts: Date.now() - CACHE_TTL_MS + 30000 });
|
|
69
|
+
return { error: "analytics computation failed" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
// ── SQLite helpers ───────────────────────────────────────
|
|
56
74
|
|
|
57
75
|
function openDB(path) {
|
|
@@ -735,11 +753,371 @@ function apiAnalytics() {
|
|
|
735
753
|
};
|
|
736
754
|
}
|
|
737
755
|
|
|
756
|
+
// ── Category Analytics ──────────────────────────────────
|
|
757
|
+
|
|
758
|
+
function apiCategoryAnalytics() {
|
|
759
|
+
// 1. Category distribution
|
|
760
|
+
const rawCategoryCounts = queryAllSessionDBs(db =>
|
|
761
|
+
safeAll(db, `SELECT category, type, COUNT(*) as count FROM session_events GROUP BY category, type ORDER BY count DESC`)
|
|
762
|
+
);
|
|
763
|
+
// Add composite key for merge
|
|
764
|
+
const catTypeRows = mergeByKey(
|
|
765
|
+
rawCategoryCounts.map(r => ({ ...r, _cat_type: `${r.category}::${r.type}` })),
|
|
766
|
+
"_cat_type",
|
|
767
|
+
(a, b) => ({ _cat_type: a._cat_type, category: a.category, type: a.type, count: a.count + b.count })
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const CATEGORY_MAP = {
|
|
771
|
+
file: ["file_read", "file_write", "file_edit", "file_glob", "file_search"],
|
|
772
|
+
git: ["git"],
|
|
773
|
+
error: ["error_tool"],
|
|
774
|
+
subagent: ["subagent_launched", "subagent_completed"],
|
|
775
|
+
"rejected-approach": ["rejected"],
|
|
776
|
+
latency: ["tool_latency"],
|
|
777
|
+
decision: ["decision", "decision_question"],
|
|
778
|
+
skill: ["skill"],
|
|
779
|
+
rule: ["rule", "rule_content"],
|
|
780
|
+
plan: ["plan_enter", "plan_exit", "plan_approved", "plan_rejected", "plan_file_write"],
|
|
781
|
+
intent: ["intent"],
|
|
782
|
+
"blocked-on": ["blocker", "blocker_resolved"],
|
|
783
|
+
constraint: ["constraint_discovered"],
|
|
784
|
+
"user-prompt": ["user_prompt"],
|
|
785
|
+
"error-resolution": ["error_resolved"],
|
|
786
|
+
"iteration-loop": ["retry_detected"],
|
|
787
|
+
env: ["env", "worktree"],
|
|
788
|
+
task: ["task_create", "task_update"],
|
|
789
|
+
mcp: ["mcp"],
|
|
790
|
+
"agent-finding": ["agent_finding"],
|
|
791
|
+
"external-ref": ["external_ref"],
|
|
792
|
+
role: ["role"],
|
|
793
|
+
cwd: ["cwd"],
|
|
794
|
+
data: ["data"],
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const typeCountMap = new Map();
|
|
798
|
+
for (const row of catTypeRows) {
|
|
799
|
+
typeCountMap.set(`${row.category}::${row.type}`, row.count);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const categories = Object.entries(CATEGORY_MAP).map(([cat, types]) => {
|
|
803
|
+
const typesObj = {};
|
|
804
|
+
let total = 0;
|
|
805
|
+
for (const t of types) {
|
|
806
|
+
const c = typeCountMap.get(`${cat}::${t}`) || 0;
|
|
807
|
+
typesObj[t] = c;
|
|
808
|
+
total += c;
|
|
809
|
+
}
|
|
810
|
+
return { category: cat, count: total, types: typesObj };
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// 2. Error intelligence
|
|
814
|
+
const errorResolution = queryAllSessionDBs(db =>
|
|
815
|
+
safeAll(db, `SELECT
|
|
816
|
+
SUM(CASE WHEN category = 'error' THEN 1 ELSE 0 END) as total_errors,
|
|
817
|
+
SUM(CASE WHEN category = 'error-resolution' THEN 1 ELSE 0 END) as resolved_errors
|
|
818
|
+
FROM session_events`)
|
|
819
|
+
);
|
|
820
|
+
const totalErrors = errorResolution.reduce((s, r) => s + (r.total_errors || 0), 0);
|
|
821
|
+
const resolvedErrors = errorResolution.reduce((s, r) => s + (r.resolved_errors || 0), 0);
|
|
822
|
+
const resolutionRate = totalErrors > 0 ? Math.round(1000 * resolvedErrors / totalErrors) / 10 : 0;
|
|
823
|
+
|
|
824
|
+
const retryStorms = queryAllSessionDBs(db =>
|
|
825
|
+
safeAll(db, `SELECT session_id, COUNT(*) as retries FROM session_events WHERE category = 'iteration-loop' GROUP BY session_id HAVING COUNT(*) > 3`)
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
const latencyData = queryAllSessionDBs(db =>
|
|
829
|
+
safeAll(db, `SELECT data FROM session_events WHERE category = 'latency'`)
|
|
830
|
+
);
|
|
831
|
+
const latencies = [];
|
|
832
|
+
const latencyByToolMap = new Map();
|
|
833
|
+
for (const row of latencyData) {
|
|
834
|
+
if (!row.data) continue;
|
|
835
|
+
const match = String(row.data).match(/^(.+?):\s*(\d+)\s*(?:ms)?$/);
|
|
836
|
+
if (!match) continue;
|
|
837
|
+
const tool = match[1].trim();
|
|
838
|
+
const ms = parseInt(match[2], 10);
|
|
839
|
+
if (isNaN(ms)) continue;
|
|
840
|
+
latencies.push(ms);
|
|
841
|
+
if (!latencyByToolMap.has(tool)) latencyByToolMap.set(tool, { sum: 0, count: 0, max: 0 });
|
|
842
|
+
const entry = latencyByToolMap.get(tool);
|
|
843
|
+
entry.sum += ms;
|
|
844
|
+
entry.count += 1;
|
|
845
|
+
entry.max = Math.max(entry.max, ms);
|
|
846
|
+
}
|
|
847
|
+
latencies.sort((a, b) => a - b);
|
|
848
|
+
const avgLatencyMs = latencies.length > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0;
|
|
849
|
+
const p95LatencyMs = latencies.length > 0 ? latencies[Math.floor(latencies.length * 0.95)] : 0;
|
|
850
|
+
|
|
851
|
+
const latencyByTool = [...latencyByToolMap.entries()]
|
|
852
|
+
.map(([tool, e]) => ({ tool, avg_ms: Math.round(e.sum / e.count), count: e.count, max_ms: e.max }))
|
|
853
|
+
.sort((a, b) => b.avg_ms - a.avg_ms);
|
|
854
|
+
|
|
855
|
+
let slowestTool = latencyByTool.length > 0 ? latencyByTool[0].tool : null;
|
|
856
|
+
|
|
857
|
+
const topErrorTools = queryAllSessionDBs(db =>
|
|
858
|
+
safeAll(db, `SELECT
|
|
859
|
+
CASE
|
|
860
|
+
WHEN data LIKE '%Bash%' THEN 'Bash'
|
|
861
|
+
WHEN data LIKE '%Read%' THEN 'Read'
|
|
862
|
+
WHEN data LIKE '%Edit%' THEN 'Edit'
|
|
863
|
+
WHEN data LIKE '%Write%' THEN 'Write'
|
|
864
|
+
WHEN data LIKE '%Agent%' THEN 'Agent'
|
|
865
|
+
ELSE substr(data, 1, 30)
|
|
866
|
+
END as tool,
|
|
867
|
+
COUNT(*) as count
|
|
868
|
+
FROM session_events WHERE category = 'error'
|
|
869
|
+
GROUP BY tool ORDER BY count DESC LIMIT 5`)
|
|
870
|
+
);
|
|
871
|
+
const mergedTopErrorTools = mergeByKey(topErrorTools, "tool", (a, b) => ({ tool: a.tool, count: a.count + b.count }))
|
|
872
|
+
.sort((a, b) => b.count - a.count).slice(0, 5);
|
|
873
|
+
|
|
874
|
+
const errorIntelligence = {
|
|
875
|
+
totalErrors,
|
|
876
|
+
resolvedErrors,
|
|
877
|
+
resolutionRate,
|
|
878
|
+
retryStorms: retryStorms.length,
|
|
879
|
+
avgLatencyMs,
|
|
880
|
+
p95LatencyMs,
|
|
881
|
+
p95SampleCount: latencies.length,
|
|
882
|
+
slowestTool,
|
|
883
|
+
topErrorTools: mergedTopErrorTools,
|
|
884
|
+
latencyByTool,
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// 3. Delegation metrics
|
|
888
|
+
const subagentCat = categories.find(c => c.category === "subagent");
|
|
889
|
+
const launched = subagentCat ? (subagentCat.types.subagent_launched || 0) : 0;
|
|
890
|
+
let completed = subagentCat ? (subagentCat.types.subagent_completed || 0) : 0;
|
|
891
|
+
if (completed > launched && launched > 0) completed = launched; // cap anomaly
|
|
892
|
+
const completionRate = launched > 0 ? Math.round(1000 * completed / launched) / 10 : 0;
|
|
893
|
+
|
|
894
|
+
// Parallel bursts: sessions with >1 subagent_launched in same session
|
|
895
|
+
const parallelBurstData = queryAllSessionDBs(db =>
|
|
896
|
+
safeAll(db, `SELECT session_id, COUNT(*) as cnt FROM session_events WHERE type = 'subagent_launched' GROUP BY session_id HAVING cnt > 1`)
|
|
897
|
+
);
|
|
898
|
+
const parallelBursts = parallelBurstData.length;
|
|
899
|
+
const maxConcurrent = parallelBurstData.reduce((m, r) => Math.max(m, r.cnt || 0), 0);
|
|
900
|
+
// Rough estimate: each completed subagent saves ~2 min
|
|
901
|
+
const timeSavedMin = Math.round(completed * 2);
|
|
902
|
+
|
|
903
|
+
const delegation = { launched, completed, completionRate, parallelBursts, maxConcurrent, timeSavedMin };
|
|
904
|
+
|
|
905
|
+
// 4. Governance
|
|
906
|
+
const rejectedData = queryAllSessionDBs(db =>
|
|
907
|
+
safeAll(db, `SELECT data FROM session_events WHERE category = 'rejected-approach'`)
|
|
908
|
+
);
|
|
909
|
+
const rejectedToolMap = new Map();
|
|
910
|
+
for (const row of rejectedData) {
|
|
911
|
+
if (!row.data) continue;
|
|
912
|
+
const tool = String(row.data).split(":")[0].trim() || "unknown";
|
|
913
|
+
rejectedToolMap.set(tool, (rejectedToolMap.get(tool) || 0) + 1);
|
|
914
|
+
}
|
|
915
|
+
const topRejected = [...rejectedToolMap.entries()]
|
|
916
|
+
.map(([tool, count]) => ({ tool, count }))
|
|
917
|
+
.sort((a, b) => b.count - a.count)
|
|
918
|
+
.slice(0, 5);
|
|
919
|
+
|
|
920
|
+
const planCat = categories.find(c => c.category === "plan");
|
|
921
|
+
const planApproved = planCat ? (planCat.types.plan_approved || 0) : 0;
|
|
922
|
+
const planRejected = planCat ? (planCat.types.plan_rejected || 0) : 0;
|
|
923
|
+
const totalPlans = planApproved + planRejected;
|
|
924
|
+
const planApprovalRate = totalPlans > 0 ? Math.round(1000 * planApproved / totalPlans) / 10 : 0;
|
|
925
|
+
|
|
926
|
+
const rejectedCat = categories.find(c => c.category === "rejected-approach");
|
|
927
|
+
const decisionCat = categories.find(c => c.category === "decision");
|
|
928
|
+
const constraintCat = categories.find(c => c.category === "constraint");
|
|
929
|
+
|
|
930
|
+
const governance = {
|
|
931
|
+
totalRejections: rejectedCat ? rejectedCat.count : 0,
|
|
932
|
+
totalDecisions: decisionCat ? decisionCat.count : 0,
|
|
933
|
+
totalConstraints: constraintCat ? constraintCat.count : 0,
|
|
934
|
+
planApproved,
|
|
935
|
+
planRejected,
|
|
936
|
+
planApprovalRate,
|
|
937
|
+
topRejected,
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// 5. Git productivity
|
|
941
|
+
const gitOps = queryAllSessionDBs(db =>
|
|
942
|
+
safeAll(db, `SELECT data as operation, COUNT(*) as count FROM session_events WHERE category = 'git' AND data IS NOT NULL AND data != '' GROUP BY data ORDER BY count DESC`)
|
|
943
|
+
);
|
|
944
|
+
const mergedGitOps = mergeByKey(gitOps, "operation", (a, b) => ({ operation: a.operation, count: a.count + b.count }))
|
|
945
|
+
.sort((a, b) => b.count - a.count);
|
|
946
|
+
const totalCommits = mergedGitOps.find(o => o.operation === "commit")?.count || 0;
|
|
947
|
+
const totalPushes = mergedGitOps.find(o => o.operation === "push")?.count || 0;
|
|
948
|
+
const totalGitOps = mergedGitOps.reduce((s, o) => s + o.count, 0);
|
|
949
|
+
|
|
950
|
+
const gitProductivity = {
|
|
951
|
+
totalCommits,
|
|
952
|
+
totalPushes,
|
|
953
|
+
commitPushRatio: totalPushes > 0 ? Math.round(100 * totalCommits / totalPushes) / 100 : totalCommits,
|
|
954
|
+
totalOperations: totalGitOps,
|
|
955
|
+
operationMix: mergedGitOps,
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// 6. Context health
|
|
959
|
+
const uniqueSkills = queryAllSessionDBs(db =>
|
|
960
|
+
safeAll(db, `SELECT DISTINCT data as skill FROM session_events WHERE category = 'skill' AND data != ''`)
|
|
961
|
+
);
|
|
962
|
+
const skillSet = [...new Set(uniqueSkills.map(r => r.skill).filter(Boolean))];
|
|
963
|
+
|
|
964
|
+
const modeDistribution = queryAllSessionDBs(db =>
|
|
965
|
+
safeAll(db, `SELECT data as mode, COUNT(*) as count FROM session_events WHERE category = 'intent' AND data != '' GROUP BY data ORDER BY count DESC`)
|
|
966
|
+
);
|
|
967
|
+
const mergedModes = mergeByKey(modeDistribution, "mode", (a, b) => ({ mode: a.mode, count: a.count + b.count }))
|
|
968
|
+
.sort((a, b) => b.count - a.count);
|
|
969
|
+
const totalModeEvents = mergedModes.reduce((s, m) => s + m.count, 0);
|
|
970
|
+
const modesWithPct = mergedModes.map(m => ({ ...m, pct: totalModeEvents > 0 ? Math.round(1000 * m.count / totalModeEvents) / 10 : 0 }));
|
|
971
|
+
|
|
972
|
+
const blockerData = queryAllSessionDBs(db =>
|
|
973
|
+
safeAll(db, `SELECT type, COUNT(*) as count FROM session_events WHERE category = 'blocked-on' GROUP BY type`)
|
|
974
|
+
);
|
|
975
|
+
const mergedBlockers = mergeByKey(blockerData, "type", (a, b) => ({ type: a.type, count: a.count + b.count }));
|
|
976
|
+
const totalBlockers = mergedBlockers.find(b => b.type === "blocker")?.count || 0;
|
|
977
|
+
const resolvedBlockers = mergedBlockers.find(b => b.type === "blocker_resolved")?.count || 0;
|
|
978
|
+
|
|
979
|
+
const ruleCat = categories.find(c => c.category === "rule");
|
|
980
|
+
const ruleCount = ruleCat ? ruleCat.count : 0;
|
|
981
|
+
|
|
982
|
+
// Unique rule files: count distinct data values for rule category
|
|
983
|
+
const uniqueRuleFiles = queryAllSessionDBs(db =>
|
|
984
|
+
safeAll(db, `SELECT DISTINCT data FROM session_events WHERE category = 'rule' AND type = 'rule' AND data IS NOT NULL AND data != ''`)
|
|
985
|
+
);
|
|
986
|
+
const uniqueRuleCount = new Set(uniqueRuleFiles.map(r => r.data)).size;
|
|
987
|
+
|
|
988
|
+
// Sessions + compacts in one query (avoids extra DB open cycle)
|
|
989
|
+
const sessionAgg = queryAllSessionDBs(db =>
|
|
990
|
+
safeAll(db, `SELECT COUNT(*) as cnt, COALESCE(SUM(compact_count), 0) as compacts FROM session_meta`)
|
|
991
|
+
);
|
|
992
|
+
const totalSessions = sessionAgg.reduce((s, r) => s + (r.cnt || 0), 0);
|
|
993
|
+
const totalCompacts = sessionAgg.reduce((s, r) => s + (r.compacts || 0), 0);
|
|
994
|
+
const compactRate = totalSessions > 0 ? Math.round(100 * totalCompacts / totalSessions) : 0;
|
|
995
|
+
|
|
996
|
+
const contextHealth = {
|
|
997
|
+
uniqueRuleFiles: uniqueRuleCount,
|
|
998
|
+
ruleLoadsPerSession: totalSessions > 0 ? Math.round(100 * ruleCount / totalSessions) / 100 : 0,
|
|
999
|
+
uniqueSkills: skillSet.length,
|
|
1000
|
+
skillList: skillSet,
|
|
1001
|
+
modeDistribution: modesWithPct,
|
|
1002
|
+
compactRate,
|
|
1003
|
+
totalBlockers,
|
|
1004
|
+
resolvedBlockers,
|
|
1005
|
+
blockerResolutionRate: totalBlockers > 0 ? Math.round(1000 * resolvedBlockers / totalBlockers) / 10 : 0,
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// 7. File activity intelligence
|
|
1009
|
+
const fileStats = queryAllSessionDBs(db =>
|
|
1010
|
+
safeAll(db, `SELECT
|
|
1011
|
+
SUM(CASE WHEN type = 'file_read' THEN 1 ELSE 0 END) as reads,
|
|
1012
|
+
SUM(CASE WHEN type IN ('file_write','file_edit') THEN 1 ELSE 0 END) as writes,
|
|
1013
|
+
SUM(CASE WHEN type IN ('file_glob','file_search') THEN 1 ELSE 0 END) as exploration,
|
|
1014
|
+
COUNT(*) as total
|
|
1015
|
+
FROM session_events WHERE category = 'file'`)
|
|
1016
|
+
);
|
|
1017
|
+
const reads = fileStats.reduce((s, r) => s + (r.reads || 0), 0);
|
|
1018
|
+
const writes = fileStats.reduce((s, r) => s + (r.writes || 0), 0);
|
|
1019
|
+
const exploration = fileStats.reduce((s, r) => s + (r.exploration || 0), 0);
|
|
1020
|
+
const totalFileEvents = fileStats.reduce((s, r) => s + (r.total || 0), 0);
|
|
1021
|
+
|
|
1022
|
+
const hotFiles = queryAllSessionDBs(db =>
|
|
1023
|
+
safeAll(db, `SELECT data as file, COUNT(*) as touches
|
|
1024
|
+
FROM session_events
|
|
1025
|
+
WHERE category = 'file' AND type IN ('file_read','file_edit','file_write') AND data != ''
|
|
1026
|
+
GROUP BY data HAVING COUNT(*) > 3
|
|
1027
|
+
ORDER BY touches DESC LIMIT 10`)
|
|
1028
|
+
);
|
|
1029
|
+
const mergedHotFiles = mergeByKey(hotFiles, "file", (a, b) => ({ file: a.file, touches: a.touches + b.touches }))
|
|
1030
|
+
.filter(f => f.touches > 3)
|
|
1031
|
+
.sort((a, b) => b.touches - a.touches)
|
|
1032
|
+
.slice(0, 10);
|
|
1033
|
+
|
|
1034
|
+
// Unique edited + read files in one query for churn rate
|
|
1035
|
+
const fileChurnData = queryAllSessionDBs(db =>
|
|
1036
|
+
safeAll(db, `SELECT
|
|
1037
|
+
COUNT(DISTINCT CASE WHEN type IN ('file_write','file_edit') THEN data END) as edited,
|
|
1038
|
+
COUNT(DISTINCT CASE WHEN type = 'file_read' THEN data END) as read_files
|
|
1039
|
+
FROM session_events WHERE category = 'file' AND data != ''`)
|
|
1040
|
+
);
|
|
1041
|
+
const uniqueEditedCount = fileChurnData.reduce((s, r) => s + (r.edited || 0), 0);
|
|
1042
|
+
const uniqueReadCount = fileChurnData.reduce((s, r) => s + (r.read_files || 0), 0);
|
|
1043
|
+
|
|
1044
|
+
const fileIntelligence = {
|
|
1045
|
+
readWriteRatio: writes > 0 ? Math.round(100 * reads / writes) / 100 : reads,
|
|
1046
|
+
explorationDepth: totalFileEvents > 0 ? Math.round(1000 * exploration / totalFileEvents) / 10 : 0,
|
|
1047
|
+
hotFiles: mergedHotFiles,
|
|
1048
|
+
fileChurnRate: uniqueReadCount > 0 ? Math.round(100 * uniqueEditedCount / uniqueReadCount) / 100 : 0,
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// 8. Composite scores (0-100)
|
|
1052
|
+
const totalEvents = categories.reduce((s, c) => s + c.count, 0);
|
|
1053
|
+
|
|
1054
|
+
// Productivity
|
|
1055
|
+
const commitSessions = queryAllSessionDBs(db =>
|
|
1056
|
+
safeAll(db, `SELECT COUNT(DISTINCT session_id) as cnt FROM session_events WHERE category = 'git' AND data = 'commit'`)
|
|
1057
|
+
);
|
|
1058
|
+
const sessionsWithCommits = commitSessions.reduce((s, r) => s + (r.cnt || 0), 0);
|
|
1059
|
+
const commitRate = totalSessions > 0 ? (sessionsWithCommits / totalSessions) * 100 : 0;
|
|
1060
|
+
// delegationRate: ratio of sessions with subagents (not events — avoids tiny % problem)
|
|
1061
|
+
const delegationRate = totalSessions > 0 ? Math.min(100, (launched / totalSessions) * 100) : 0;
|
|
1062
|
+
// fileChurnRate: unique edited / unique read (not count/events — fixes dimensional mismatch)
|
|
1063
|
+
const fileChurnForScore = uniqueReadCount > 0 ? Math.min(100, (uniqueEditedCount / uniqueReadCount) * 100) : 0;
|
|
1064
|
+
const productivityScore = Math.min(100, Math.round(
|
|
1065
|
+
(commitRate * 0.3) + (delegationRate * 0.2) + ((100 - fileChurnForScore) * 0.2) + (resolutionRate * 0.3)
|
|
1066
|
+
));
|
|
1067
|
+
|
|
1068
|
+
// Quality — zero errors = perfect quality (100), not penalized
|
|
1069
|
+
const retryRate = totalSessions > 0 ? (retryStorms.length / totalSessions) * 100 : 0;
|
|
1070
|
+
const errorRate = totalEvents > 0 ? (totalErrors / totalEvents) * 100 : 0;
|
|
1071
|
+
const effectiveResolution = totalErrors === 0 ? 100 : resolutionRate; // no errors = perfect
|
|
1072
|
+
const qualityScore = Math.min(100, Math.round(
|
|
1073
|
+
(effectiveResolution * 0.4) + ((100 - retryRate) * 0.3) + ((100 - errorRate) * 0.3)
|
|
1074
|
+
));
|
|
1075
|
+
|
|
1076
|
+
// Delegation
|
|
1077
|
+
const agentFindingCat = categories.find(c => c.category === "agent-finding");
|
|
1078
|
+
const findingCount = agentFindingCat ? agentFindingCat.count : 0;
|
|
1079
|
+
const hasBursts = parallelBursts > 0 ? 100 : 0;
|
|
1080
|
+
const delegationScore = Math.min(100, Math.round(
|
|
1081
|
+
(completionRate * 0.5) + (hasBursts * 0.3) + (Math.min(findingCount / 5, 1) * 20)
|
|
1082
|
+
));
|
|
1083
|
+
|
|
1084
|
+
// Context Health
|
|
1085
|
+
const ruleFreshness = uniqueRuleCount > 0 ? 100 : 0;
|
|
1086
|
+
const skillDiversity = Math.min(skillSet.length * 20, 100);
|
|
1087
|
+
const planApprovalForScore = (planApproved + planRejected) > 0 ? planApprovalRate : 50;
|
|
1088
|
+
const modeBalance = mergedModes.length > 1 ? 100 : 50;
|
|
1089
|
+
const contextHealthScore = Math.min(100, Math.round(
|
|
1090
|
+
(ruleFreshness * 0.3) + (skillDiversity * 0.2) + (planApprovalForScore * 0.25) + (modeBalance * 0.25)
|
|
1091
|
+
));
|
|
1092
|
+
|
|
1093
|
+
const compositeScores = {
|
|
1094
|
+
productivity: productivityScore,
|
|
1095
|
+
quality: qualityScore,
|
|
1096
|
+
delegation: delegationScore,
|
|
1097
|
+
contextHealth: contextHealthScore,
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const insufficientData = totalEvents < 50 || totalSessions < 3;
|
|
1101
|
+
|
|
1102
|
+
return {
|
|
1103
|
+
categories,
|
|
1104
|
+
errorIntelligence,
|
|
1105
|
+
delegation,
|
|
1106
|
+
governance,
|
|
1107
|
+
gitProductivity,
|
|
1108
|
+
contextHealth,
|
|
1109
|
+
fileIntelligence,
|
|
1110
|
+
compositeScores,
|
|
1111
|
+
insufficientData,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
738
1115
|
// ── Router ───────────────────────────────────────────────
|
|
739
1116
|
|
|
740
1117
|
function route(method, pathname, params) {
|
|
741
1118
|
if (pathname === "/api/overview") return apiOverview();
|
|
742
|
-
if (pathname === "/api/analytics") return apiAnalytics
|
|
1119
|
+
if (pathname === "/api/analytics") return cached("analytics", apiAnalytics);
|
|
1120
|
+
if (pathname === "/api/category-analytics") return cached("category-analytics", apiCategoryAnalytics);
|
|
743
1121
|
if (pathname === "/api/content") return apiContentDBs();
|
|
744
1122
|
if (pathname === "/api/sessions") return apiSessionDBs();
|
|
745
1123
|
|
package/insight/src/lib/api.ts
CHANGED
|
@@ -16,36 +16,37 @@ export interface SessionEventData { events: SessionEvent[]; resume: { snapshot:
|
|
|
16
16
|
export interface AnalyticsData {
|
|
17
17
|
totals: {
|
|
18
18
|
totalSessions: number; totalEvents: number; avgSessionMin: number;
|
|
19
|
-
totalErrors: number;
|
|
20
|
-
|
|
19
|
+
totalErrors: number; errorRate: number; totalCompacts: number;
|
|
20
|
+
compactRate: number; reads: number; writes: number;
|
|
21
|
+
readWriteRatio: number; totalFileOps: number;
|
|
22
|
+
totalSubagents: number; totalTasks: number;
|
|
23
|
+
totalPrompts: number; promptsPerSession: number;
|
|
24
|
+
uniqueProjects: number;
|
|
21
25
|
totalCommits: number; commitsPerSession: number; sandboxRate: number;
|
|
22
26
|
totalRules: number; totalEditTestCycles: number;
|
|
23
27
|
};
|
|
24
28
|
sessionsByDate: { date: string; count: number; events: number; compacts: number }[];
|
|
25
|
-
sessionDurations: { session_id: string; project_dir: string; started_at: string; duration_min: number; event_count: number; compact_count: number }[];
|
|
26
|
-
intents: { intent: string; count: number }[];
|
|
27
|
-
eventTypes: { type: string; count: number }[];
|
|
28
|
-
errorRates: { session_id: string; started_at: string; errors: number; total: number; error_rate: number }[];
|
|
29
|
-
fileActivity: { file: string; count: number }[];
|
|
29
|
+
sessionDurations: { session_id: string; project_dir: string; started_at: string; last_event_at: string; duration_min: number; event_count: number; compact_count: number }[];
|
|
30
30
|
toolUsage: { tool: string; count: number }[];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
total: number; bursts: number; maxConcurrent: number;
|
|
35
|
-
parallelCount: number; sequentialCount: number; timeSavedMin: number;
|
|
36
|
-
burstDetails: { size: number; time: string }[];
|
|
37
|
-
};
|
|
31
|
+
mcpTools: { tool: string; count: number }[];
|
|
32
|
+
errors: { detail: string; created_at: string; session_id: string }[];
|
|
33
|
+
fileActivity: { file: string; op: string; count: number }[];
|
|
38
34
|
workModes: { mode: string; count: number }[];
|
|
39
35
|
timeToFirstCommit: { session_id: string; started_at: string; first_commit_at: string; minutes_to_commit: number }[];
|
|
40
36
|
exploreExecRatio: { explore: number; execute: number; total: number };
|
|
41
37
|
reworkData: { session_id: string; file: string; edit_count: number }[];
|
|
42
38
|
gitActivity: { action: string; created_at: string; session_id: string; project_dir: string; session_start: string }[];
|
|
43
|
-
|
|
39
|
+
subagents: {
|
|
40
|
+
total: number; bursts: number; maxConcurrent: number;
|
|
41
|
+
parallelCount: number; sequentialCount: number; timeSavedMin: number;
|
|
42
|
+
burstDetails: { size: number; time: string }[];
|
|
43
|
+
};
|
|
44
|
+
projectActivity: { project_dir: string; sessions: number; events: number; compacts: number; avg_confidence?: number; high_conf_events?: number }[];
|
|
44
45
|
attribution?: { totalEvents: number; attributedEvents: number; unknownEvents: number; unknownPct: number; avgConfidencePct: number; highConfidencePct: number; isFallbackOnly: boolean };
|
|
45
46
|
hourlyPattern: { hour: number; count: number }[];
|
|
46
47
|
weeklyTrend: { week: string; sessions: number; events: number }[];
|
|
47
48
|
tasks: { task: string; created_at: string }[];
|
|
48
|
-
prompts: { prompt: string; created_at: string }[];
|
|
49
|
+
prompts: { prompt: string; created_at: string; session_id: string }[];
|
|
49
50
|
masteryTrend: { week: string; errors: number; total: number; error_rate: number }[];
|
|
50
51
|
commitRate: { session_id: string; project_dir: string; commits: number }[];
|
|
51
52
|
sandboxAdoption: { sandbox_calls: number; total_calls: number };
|
|
@@ -53,6 +54,76 @@ export interface AnalyticsData {
|
|
|
53
54
|
editTestCycles: { session_id: string; cycles: number }[];
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export interface CategoryCount {
|
|
58
|
+
category: string;
|
|
59
|
+
count: number;
|
|
60
|
+
types: Record<string, number>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CategoryAnalyticsData {
|
|
64
|
+
categories: CategoryCount[];
|
|
65
|
+
errorIntelligence: {
|
|
66
|
+
totalErrors: number;
|
|
67
|
+
resolvedErrors: number;
|
|
68
|
+
resolutionRate: number;
|
|
69
|
+
retryStorms: number;
|
|
70
|
+
avgLatencyMs: number;
|
|
71
|
+
p95LatencyMs: number;
|
|
72
|
+
p95SampleCount: number;
|
|
73
|
+
slowestTool: string | null;
|
|
74
|
+
topErrorTools: { tool: string; count: number }[];
|
|
75
|
+
latencyByTool: { tool: string; avg_ms: number; count: number; max_ms: number }[];
|
|
76
|
+
};
|
|
77
|
+
delegation: {
|
|
78
|
+
launched: number;
|
|
79
|
+
completed: number;
|
|
80
|
+
completionRate: number;
|
|
81
|
+
parallelBursts: number;
|
|
82
|
+
maxConcurrent: number;
|
|
83
|
+
timeSavedMin: number;
|
|
84
|
+
};
|
|
85
|
+
governance: {
|
|
86
|
+
totalRejections: number;
|
|
87
|
+
totalDecisions: number;
|
|
88
|
+
totalConstraints: number;
|
|
89
|
+
planApproved: number;
|
|
90
|
+
planRejected: number;
|
|
91
|
+
planApprovalRate: number;
|
|
92
|
+
topRejected: { tool: string; count: number }[];
|
|
93
|
+
};
|
|
94
|
+
gitProductivity: {
|
|
95
|
+
totalCommits: number;
|
|
96
|
+
totalPushes: number;
|
|
97
|
+
commitPushRatio: number;
|
|
98
|
+
totalOperations: number;
|
|
99
|
+
operationMix: { operation: string; count: number }[];
|
|
100
|
+
};
|
|
101
|
+
contextHealth: {
|
|
102
|
+
uniqueRuleFiles: number;
|
|
103
|
+
ruleLoadsPerSession: number;
|
|
104
|
+
uniqueSkills: number;
|
|
105
|
+
skillList: string[];
|
|
106
|
+
modeDistribution: { mode: string; count: number; pct: number }[];
|
|
107
|
+
compactRate: number;
|
|
108
|
+
totalBlockers: number;
|
|
109
|
+
resolvedBlockers: number;
|
|
110
|
+
blockerResolutionRate: number;
|
|
111
|
+
};
|
|
112
|
+
fileIntelligence: {
|
|
113
|
+
readWriteRatio: number;
|
|
114
|
+
explorationDepth: number;
|
|
115
|
+
hotFiles: { file: string; touches: number }[];
|
|
116
|
+
fileChurnRate: number;
|
|
117
|
+
};
|
|
118
|
+
compositeScores: {
|
|
119
|
+
productivity: number;
|
|
120
|
+
quality: number;
|
|
121
|
+
delegation: number;
|
|
122
|
+
contextHealth: number;
|
|
123
|
+
};
|
|
124
|
+
insufficientData: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
56
127
|
async function get<T>(path: string): Promise<T> {
|
|
57
128
|
const r = await fetch(`${API}${path}`);
|
|
58
129
|
return r.json() as Promise<T>;
|
|
@@ -61,6 +132,7 @@ async function get<T>(path: string): Promise<T> {
|
|
|
61
132
|
export const api = {
|
|
62
133
|
overview: () => get<OverviewData>("/overview"),
|
|
63
134
|
analytics: () => get<AnalyticsData>("/analytics"),
|
|
135
|
+
categoryAnalytics: () => get<CategoryAnalyticsData>("/category-analytics"),
|
|
64
136
|
content: () => get<ContentDB[]>("/content"),
|
|
65
137
|
chunks: (dbHash: string, sourceId: number) => get<Chunk[]>(`/content/${dbHash}/chunks/${sourceId}`),
|
|
66
138
|
search: (q: string) => get<Chunk[]>(`/search?q=${encodeURIComponent(q)}`),
|