context-mode 1.0.100 → 1.0.103
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/server.js +4 -3
- package/cli.bundle.mjs +107 -107
- package/hooks/ensure-deps.mjs +35 -7
- 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 +90 -90
- package/start.mjs +3 -1
package/hooks/ensure-deps.mjs
CHANGED
|
@@ -33,10 +33,12 @@ const NATIVE_BINARIES = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Check if the current runtime has built-in SQLite support
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* Check if the current runtime has built-in SQLite support.
|
|
37
|
+
* Bun has bun:sqlite, Node >= 22.5 has node:sqlite.
|
|
38
|
+
*
|
|
39
|
+
* Used to skip the SIGSEGV-prone child-process probe on modern Node (#331),
|
|
40
|
+
* but NOT to skip installing better-sqlite3 — the bundle unconditionally
|
|
41
|
+
* requires it as a fallback on non-Linux platforms (#371).
|
|
40
42
|
*/
|
|
41
43
|
function hasModernSqlite() {
|
|
42
44
|
if (typeof globalThis.Bun !== "undefined") return true;
|
|
@@ -45,7 +47,8 @@ function hasModernSqlite() {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function ensureDeps() {
|
|
48
|
-
|
|
50
|
+
// Bun ships bun:sqlite and never needs better-sqlite3
|
|
51
|
+
if (typeof globalThis.Bun !== "undefined") return;
|
|
49
52
|
for (const pkg of NATIVE_DEPS) {
|
|
50
53
|
const pkgDir = resolve(root, "node_modules", pkg);
|
|
51
54
|
if (!existsSync(pkgDir)) {
|
|
@@ -95,7 +98,13 @@ function probeNativeInChildProcess(pluginRoot) {
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
export function ensureNativeCompat(pluginRoot) {
|
|
98
|
-
|
|
101
|
+
// Bun ships bun:sqlite — no native addon needed
|
|
102
|
+
if (typeof globalThis.Bun !== "undefined") return;
|
|
103
|
+
|
|
104
|
+
// On Node >= 22.5, skip the child-process probe that can cause SIGSEGV (#331).
|
|
105
|
+
// The binary install/rebuild still runs — only the dlopen probe is skipped.
|
|
106
|
+
const skipProbe = hasModernSqlite();
|
|
107
|
+
|
|
99
108
|
try {
|
|
100
109
|
const abi = process.versions.modules;
|
|
101
110
|
const nativeDir = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release");
|
|
@@ -104,10 +113,11 @@ export function ensureNativeCompat(pluginRoot) {
|
|
|
104
113
|
|
|
105
114
|
if (!existsSync(nativeDir)) return;
|
|
106
115
|
|
|
107
|
-
// Fast path: cached binary for this ABI already exists — swap in
|
|
116
|
+
// Fast path: cached binary for this ABI already exists — swap in
|
|
108
117
|
if (existsSync(abiCachePath)) {
|
|
109
118
|
copyFileSync(abiCachePath, binaryPath);
|
|
110
119
|
codesignBinary(binaryPath);
|
|
120
|
+
if (skipProbe) return; // Trust the cached binary — skip SIGSEGV-prone probe
|
|
111
121
|
// Validate via child process — dlopen cache is per-process, so in-process
|
|
112
122
|
// require() can't detect a swapped binary on disk (#148)
|
|
113
123
|
if (probeNativeInChildProcess(pluginRoot)) {
|
|
@@ -116,6 +126,24 @@ export function ensureNativeCompat(pluginRoot) {
|
|
|
116
126
|
// Cached binary is stale/corrupt — fall through to rebuild
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
if (skipProbe) {
|
|
130
|
+
// On modern Node: if binary exists, trust it; if missing, rebuild without probing
|
|
131
|
+
if (!existsSync(binaryPath)) {
|
|
132
|
+
execSync(`${process.platform === "win32" ? "npm.cmd" : "npm"} rebuild better-sqlite3 --ignore-scripts=false`, {
|
|
133
|
+
cwd: pluginRoot,
|
|
134
|
+
stdio: "pipe",
|
|
135
|
+
timeout: 60000,
|
|
136
|
+
shell: true,
|
|
137
|
+
});
|
|
138
|
+
codesignBinary(binaryPath);
|
|
139
|
+
// Cache the rebuilt binary for this ABI
|
|
140
|
+
if (existsSync(binaryPath)) {
|
|
141
|
+
copyFileSync(binaryPath, abiCachePath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
// Probe: try loading better-sqlite3 with current Node
|
|
120
148
|
if (existsSync(binaryPath) && probeNativeInChildProcess(pluginRoot)) {
|
|
121
149
|
// Load succeeded — cache the working binary for this ABI
|
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)}`),
|