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.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +22 -8
  6. package/build/adapters/claude-code/hooks.d.ts +4 -2
  7. package/build/adapters/claude-code/hooks.js +11 -4
  8. package/build/adapters/codex/index.d.ts +1 -1
  9. package/build/adapters/codex/index.js +6 -5
  10. package/build/adapters/gemini-cli/hooks.js +2 -1
  11. package/build/adapters/jetbrains-copilot/hooks.js +2 -1
  12. package/build/adapters/kiro/hooks.js +2 -1
  13. package/build/adapters/qwen-code/index.d.ts +1 -1
  14. package/build/adapters/qwen-code/index.js +13 -10
  15. package/build/adapters/types.d.ts +13 -0
  16. package/build/adapters/types.js +23 -1
  17. package/build/adapters/vscode-copilot/hooks.js +2 -1
  18. package/build/openclaw-plugin.js +3 -2
  19. package/build/pi-extension.js +1 -1
  20. package/build/search/auto-memory.d.ts +29 -0
  21. package/build/search/auto-memory.js +121 -0
  22. package/build/search/unified.d.ts +41 -0
  23. package/build/search/unified.js +89 -0
  24. package/build/server.js +73 -24
  25. package/build/session/analytics.js +1 -1
  26. package/build/session/db.d.ts +17 -0
  27. package/build/session/db.js +28 -0
  28. package/build/session/extract.d.ts +4 -0
  29. package/build/session/extract.js +232 -1
  30. package/build/session/snapshot.js +31 -0
  31. package/build/store.js +67 -4
  32. package/build/types.d.ts +1 -0
  33. package/cli.bundle.mjs +254 -119
  34. package/configs/claude-code/CLAUDE.md +21 -1
  35. package/configs/codex/AGENTS.md +22 -1
  36. package/configs/cursor/context-mode.mdc +18 -1
  37. package/configs/gemini-cli/GEMINI.md +22 -1
  38. package/configs/jetbrains-copilot/copilot-instructions.md +22 -1
  39. package/configs/kilo/AGENTS.md +19 -2
  40. package/configs/kiro/KIRO.md +18 -1
  41. package/configs/openclaw/AGENTS.md +22 -2
  42. package/configs/opencode/AGENTS.md +18 -1
  43. package/configs/pi/AGENTS.md +18 -1
  44. package/configs/qwen-code/QWEN.md +38 -18
  45. package/configs/vscode-copilot/copilot-instructions.md +22 -1
  46. package/hooks/auto-injection.mjs +76 -0
  47. package/hooks/codex/userpromptsubmit.mjs +1 -1
  48. package/hooks/core/mcp-ready.mjs +7 -1
  49. package/hooks/ensure-deps.mjs +35 -7
  50. package/hooks/posttooluse.mjs +50 -1
  51. package/hooks/precompact.mjs +9 -0
  52. package/hooks/pretooluse.mjs +27 -0
  53. package/hooks/routing-block.mjs +7 -1
  54. package/hooks/session-db.bundle.mjs +19 -13
  55. package/hooks/session-extract.bundle.mjs +2 -2
  56. package/hooks/session-snapshot.bundle.mjs +18 -17
  57. package/hooks/sessionstart.mjs +17 -0
  58. package/hooks/userpromptsubmit.mjs +1 -1
  59. package/insight/server.mjs +379 -1
  60. package/insight/src/lib/api.ts +88 -16
  61. package/insight/src/routes/index.tsx +566 -5
  62. package/openclaw.plugin.json +1 -1
  63. package/package.json +1 -1
  64. package/server.bundle.mjs +222 -87
  65. package/start.mjs +3 -1
@@ -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
 
@@ -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; avgErrorRate: number; totalCompacts: number;
20
- uniqueFiles: number; uniqueProjects: number;
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
- gitActivity: { action: string; created_at: string }[];
32
- skillUsage: { skill: string; count: number }[];
33
- subagents: {
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
- projectActivity: { project_dir: string; sessions: number; events: number; avg_confidence?: number; high_conf_events?: number }[];
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)}`),