pi-mega-compact 0.20.79 → 0.20.80
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/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/mega-config.js +2 -0
- package/dist/extensions/mega-events/analytics-handler.js +169 -0
- package/dist/extensions/mega-events/register.js +3 -0
- package/dist/src/store/analytics/connection.js +114 -0
- package/dist/src/store/analytics/contract-compliance.test/_helpers.js +39 -0
- package/dist/src/store/analytics/index.js +25 -0
- package/dist/src/store/analytics/memory-store.js +111 -0
- package/dist/src/store/analytics/schema.js +108 -0
- package/dist/src/store/analytics/sqlite-store/admin.js +84 -0
- package/dist/src/store/analytics/sqlite-store/ctx.js +1 -0
- package/dist/src/store/analytics/sqlite-store/read.js +25 -0
- package/dist/src/store/analytics/sqlite-store/rows.js +44 -0
- package/dist/src/store/analytics/sqlite-store/write.js +90 -0
- package/dist/src/store/analytics/sqlite-store.js +79 -0
- package/dist/src/store/analytics/types.js +17 -0
- package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js → AdminTab-C-kTQx7a.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js.map → AdminTab-C-kTQx7a.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js → AreaChart-BmntvH86.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js.map → AreaChart-BmntvH86.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js → BarChart-D_AStVsb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js.map → BarChart-D_AStVsb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js → CacheTab-VRFk7EPo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js.map → CacheTab-VRFk7EPo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js → ConfigTab-Ci5LOohz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js.map → ConfigTab-Ci5LOohz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js → EventsTab-8Y7if14O.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js.map → EventsTab-8Y7if14O.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-D4eVI8ar.js → HealthTab-Cw5eYIYq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-D4eVI8ar.js.map → HealthTab-Cw5eYIYq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js → MaintenanceTab-B0o3a57B.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js.map → MaintenanceTab-B0o3a57B.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js → MemoryMapTab-JV8mLhsc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js.map → MemoryMapTab-JV8mLhsc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js → MetricsCards-Bkcv7Nl8.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js.map → MetricsCards-Bkcv7Nl8.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MetricsTab-CAG4VgbM.js +2 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CKqJ0Zl-.js.map → MetricsTab-CAG4VgbM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-gxdUR58S.js +16 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-gxdUR58S.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js → ReposTab-2hMRSS76.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js.map → ReposTab-2hMRSS76.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js → SessionsTab-Pjx5TBtW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js.map → SessionsTab-Pjx5TBtW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js → SetupTab-B6Y3eHXo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js.map → SetupTab-B6Y3eHXo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js → TimeSavedCard-BSo2lwSE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js.map → TimeSavedCard-BSo2lwSE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js → TurnMemoryView-CdGCu8Af.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js.map → TurnMemoryView-CdGCu8Af.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-Cc4kAoAR.js +2 -0
- package/extensions/dashboard-client/dist/assets/{TurnsTab-C7pm92Hg.js.map → TurnsTab-Cc4kAoAR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js → VcStatusBadge-DcLNLyy6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js.map → VcStatusBadge-DcLNLyy6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js → VectorCortexTab-n-3v91td.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js.map → VectorCortexTab-n-3v91td.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js → WikiTab-BpwWUleH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js.map → WikiTab-BpwWUleH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js → button-B38fJDBG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js.map → button-B38fJDBG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js → card-COtt-Q5y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js.map → card-COtt-Q5y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js → client-extra-D659nPqu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js.map → client-extra-D659nPqu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js → generateCategoricalChart-COFUvM7d.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js.map → generateCategoricalChart-COFUvM7d.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js → index-C3ZUU5vU.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js.map → index-C3ZUU5vU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js → switch-DvYCCZ0E.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js.map → switch-DvYCCZ0E.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js → toggle-bJGW8eFJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js.map → toggle-bJGW8eFJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js → useSSE-D-OoWxa2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js.map → useSSE-D-OoWxa2.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/components/RepoContextStack.tsx +28 -21
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/mega-config-types.ts +3 -0
- package/extensions/mega-config.ts +2 -0
- package/extensions/mega-events/analytics-handler.ts +236 -0
- package/extensions/mega-events/register.ts +3 -0
- package/package.json +1 -1
- package/src/store/analytics/connection.ts +128 -0
- package/src/store/analytics/contract-compliance.test/_helpers.ts +47 -0
- package/src/store/analytics/index.ts +61 -0
- package/src/store/analytics/memory-store.ts +161 -0
- package/src/store/analytics/schema.ts +132 -0
- package/src/store/analytics/sqlite-store/admin.ts +96 -0
- package/src/store/analytics/sqlite-store/ctx.ts +9 -0
- package/src/store/analytics/sqlite-store/read.ts +46 -0
- package/src/store/analytics/sqlite-store/rows.ts +58 -0
- package/src/store/analytics/sqlite-store/write.ts +111 -0
- package/src/store/analytics/sqlite-store.ts +119 -0
- package/src/store/analytics/types.ts +202 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-CKqJ0Zl-.js +0 -2
- package/extensions/dashboard-client/dist/assets/OverviewTab-9Kxo79mi.js +0 -16
- package/extensions/dashboard-client/dist/assets/OverviewTab-9Kxo79mi.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-C7pm92Hg.js +0 -2
|
@@ -62,6 +62,7 @@ export const SETTINGS = [
|
|
|
62
62
|
boolDirect("MEGACOMPACT_AUTO_WIKI", "Auto Wiki", "Auto-categorizing wiki (k-means + TF-IDF)", true),
|
|
63
63
|
boolDirect("MEGACOMPACT_WIKI_SEED_FROM_TURNS", "Wiki Seed from Turns", "Seed topic model from raw_transcript when chunks thin", true),
|
|
64
64
|
boolDirect("MEGACOMPACT_TURNS_DB", "Turns DB", "Isolated turns.db store", true),
|
|
65
|
+
boolDirect("MEGACOMPACT_PROVIDER_MODEL_ANALYTICS", "Provider/Model Analytics", "Capture provider/model analytics in analytics.db", true),
|
|
65
66
|
num("MEGACOMPACT_WIKI_SILHOUETTE_MIN", "Silhouette Minimum", "Full rebuild trigger threshold", 0.2, 0, 1),
|
|
66
67
|
num("MEGACOMPACT_TURNS_RETENTION_DAYS", "Turn Retention", "Per-turn provenance retention window", 30, 1, 365, "days"),
|
|
67
68
|
num("MEGACOMPACT_TURNS_KEEP_MIN", "Turns Keep Minimum", "Min turns kept per conversation during prune", 5, 1, 100),
|
|
@@ -160,6 +160,8 @@ export function loadConfig() {
|
|
|
160
160
|
// turn path (S48 behavior). Mirrors src/config/turns.ts TURNS_DB_ENABLED.
|
|
161
161
|
turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
162
162
|
autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
163
|
+
// PMA: provider/model analytics (isolated analytics.db). Default ON.
|
|
164
|
+
providerModelAnalytics: envBool("MEGACOMPACT_PROVIDER_MODEL_ANALYTICS", true),
|
|
163
165
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
164
166
|
crossRepoCosine: Number(process.env.MEGACOMPACT_CROSSREPO_COSINE ?? "0.90"),
|
|
165
167
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createAnalyticsStore, closeAllAnalyticsDbs, } from "../../src/store/analytics/index.js";
|
|
2
|
+
// ── Per-stateDir store cache (mirrors mega-turn-store.ts storeFor) ─────
|
|
3
|
+
const stores = new Map();
|
|
4
|
+
function storeFor(stateDir) {
|
|
5
|
+
let s = stores.get(stateDir);
|
|
6
|
+
if (!s) {
|
|
7
|
+
s = createAnalyticsStore({ stateDir });
|
|
8
|
+
stores.set(stateDir, s);
|
|
9
|
+
}
|
|
10
|
+
return s;
|
|
11
|
+
}
|
|
12
|
+
/** Close all cached analytics stores (test teardown / shutdown). */
|
|
13
|
+
export function closeAnalyticsStores() {
|
|
14
|
+
closeAllAnalyticsDbs();
|
|
15
|
+
stores.clear();
|
|
16
|
+
}
|
|
17
|
+
const pmaTimings = new WeakMap();
|
|
18
|
+
function getPma(rt) {
|
|
19
|
+
let s = pmaTimings.get(rt);
|
|
20
|
+
if (!s) {
|
|
21
|
+
s = { providerStart: 0, ttft: 0, correlationId: null };
|
|
22
|
+
pmaTimings.set(rt, s);
|
|
23
|
+
}
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
/** @internal Test-only accessor — production code uses getPma() in-module. */
|
|
27
|
+
export function __pmaTimingForTest(rt) {
|
|
28
|
+
return getPma(rt);
|
|
29
|
+
}
|
|
30
|
+
function usageOf(msg) {
|
|
31
|
+
if (msg.role === "assistant" && msg.usage)
|
|
32
|
+
return msg.usage;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
36
|
+
/** Build a QualityNote with provider/model availability. */
|
|
37
|
+
function qualityFor(provider, model) {
|
|
38
|
+
if (!provider || !model)
|
|
39
|
+
return { unavailable: true, note: "provider/model not captured" };
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
// ── Registration ──────────────────────────────────────────────────────
|
|
43
|
+
/** Register the analytics event handlers. Flag-gated: OFF = complete no-op. */
|
|
44
|
+
export function registerAnalyticsHandler(pi, runtime, config) {
|
|
45
|
+
if (!config.providerModelAnalytics)
|
|
46
|
+
return; // flag OFF — no DB, no handlers
|
|
47
|
+
// ── turn_start: stamp the request start + emit request_started ──────
|
|
48
|
+
pi.on("turn_start", async (event) => {
|
|
49
|
+
try {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const corrId = `req_${runtime.rt.sessionId}_${event.turnIndex}`;
|
|
52
|
+
const pma = getPma(runtime);
|
|
53
|
+
pma.correlationId = corrId;
|
|
54
|
+
pma.providerStart = 0; // reset; stamped at before_provider_request
|
|
55
|
+
pma.ttft = 0;
|
|
56
|
+
const provider = runtime.currentModel?.provider;
|
|
57
|
+
const model = runtime.currentModel?.modelId;
|
|
58
|
+
const fact = {
|
|
59
|
+
id: `${corrId}_started`,
|
|
60
|
+
correlationId: corrId,
|
|
61
|
+
sessionId: runtime.rt.sessionId,
|
|
62
|
+
turnId: String(event.turnIndex),
|
|
63
|
+
eventKind: "request_started",
|
|
64
|
+
observedAt: now,
|
|
65
|
+
provider,
|
|
66
|
+
model,
|
|
67
|
+
source: "host_adapter",
|
|
68
|
+
quality: qualityFor(provider, model),
|
|
69
|
+
};
|
|
70
|
+
storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* non-fatal: analytics never breaks the agent loop */
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// ── before_provider_request: stamp provider start + emit provider_selected
|
|
77
|
+
pi.on("before_provider_request", async () => {
|
|
78
|
+
try {
|
|
79
|
+
const pma = getPma(runtime);
|
|
80
|
+
pma.providerStart = Date.now();
|
|
81
|
+
const corrId = pma.correlationId;
|
|
82
|
+
if (!corrId)
|
|
83
|
+
return;
|
|
84
|
+
const provider = runtime.currentModel?.provider;
|
|
85
|
+
const model = runtime.currentModel?.modelId;
|
|
86
|
+
const fact = {
|
|
87
|
+
id: `${corrId}_selected`,
|
|
88
|
+
correlationId: corrId,
|
|
89
|
+
sessionId: runtime.rt.sessionId,
|
|
90
|
+
eventKind: "provider_selected",
|
|
91
|
+
observedAt: pma.providerStart,
|
|
92
|
+
provider,
|
|
93
|
+
model,
|
|
94
|
+
source: "host_adapter",
|
|
95
|
+
quality: qualityFor(provider, model),
|
|
96
|
+
};
|
|
97
|
+
storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* non-fatal */
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
// ── message_update: capture TTFT on first text token ────────────────
|
|
104
|
+
// Net-new seam — no existing handler registers message_update.
|
|
105
|
+
// Proven available in PMA-0 (agent-loop.js:201-227).
|
|
106
|
+
pi.on("message_update", async (event) => {
|
|
107
|
+
try {
|
|
108
|
+
const pma = getPma(runtime);
|
|
109
|
+
if (pma.ttft > 0)
|
|
110
|
+
return; // already captured this turn
|
|
111
|
+
if (pma.providerStart <= 0)
|
|
112
|
+
return; // no provider start stamp
|
|
113
|
+
const chunkType = event
|
|
114
|
+
.assistantMessageEvent?.type;
|
|
115
|
+
if (chunkType !== "text_start" && chunkType !== "text_delta")
|
|
116
|
+
return;
|
|
117
|
+
pma.ttft = Date.now() - pma.providerStart;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* non-fatal */
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// ── turn_end: emit terminal fact with tokens, latency, ttft, status ─
|
|
124
|
+
pi.on("turn_end", async (event) => {
|
|
125
|
+
try {
|
|
126
|
+
const pma = getPma(runtime);
|
|
127
|
+
const corrId = pma.correlationId;
|
|
128
|
+
if (!corrId)
|
|
129
|
+
return;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const durationMs = runtime.perfTurnStart > 0 ? now - runtime.perfTurnStart : undefined;
|
|
132
|
+
const u = usageOf(event.message);
|
|
133
|
+
const stopReason = event.message.stopReason;
|
|
134
|
+
const provider = runtime.currentModel?.provider;
|
|
135
|
+
const model = runtime.currentModel?.modelId;
|
|
136
|
+
const isFailed = stopReason === "error" || stopReason === "aborted";
|
|
137
|
+
const eventKind = isFailed ? "request_failed" : "request_completed";
|
|
138
|
+
const fact = {
|
|
139
|
+
id: `${corrId}_${eventKind}`,
|
|
140
|
+
correlationId: corrId,
|
|
141
|
+
sessionId: runtime.rt.sessionId,
|
|
142
|
+
turnId: String(event.turnIndex),
|
|
143
|
+
eventKind,
|
|
144
|
+
observedAt: now,
|
|
145
|
+
provider,
|
|
146
|
+
model,
|
|
147
|
+
status: stopReason ?? undefined,
|
|
148
|
+
inputTokens: u?.input,
|
|
149
|
+
outputTokens: u?.output,
|
|
150
|
+
cacheReadTokens: u?.cacheRead,
|
|
151
|
+
cacheWriteTokens: u?.cacheWrite,
|
|
152
|
+
durationMs,
|
|
153
|
+
ttftMs: pma.ttft > 0 ? pma.ttft : undefined,
|
|
154
|
+
source: "host_adapter",
|
|
155
|
+
quality: {
|
|
156
|
+
...qualityFor(provider, model),
|
|
157
|
+
...(pma.ttft > 0 ? {} : { note: "TTFT not captured (no first-token event)" }),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
|
|
161
|
+
// Reset per-turn state.
|
|
162
|
+
pma.correlationId = null;
|
|
163
|
+
pma.ttft = 0;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* non-fatal */
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -3,6 +3,7 @@ import { registerAgentHandlers } from "./agent-handlers.js";
|
|
|
3
3
|
import { registerContextHandler } from "./context-handler.js";
|
|
4
4
|
import { registerCompactHandlers } from "./compact-handlers.js";
|
|
5
5
|
import { registerPerfHandler } from "./perf-handler.js";
|
|
6
|
+
import { registerAnalyticsHandler } from "./analytics-handler.js";
|
|
6
7
|
import { registerVectorCortexRender } from "../mega-context/vector-cortex.js";
|
|
7
8
|
/**
|
|
8
9
|
* DIAG accessor for the headless test harness: the most recently constructed
|
|
@@ -37,4 +38,6 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
37
38
|
registerContextHandler(pi, runtime, config);
|
|
38
39
|
registerCompactHandlers(pi, runtime, config);
|
|
39
40
|
registerPerfHandler(pi, runtime);
|
|
41
|
+
// PMA-2: analytics ingestion — flag-gated (OFF = no-op, no DB opened).
|
|
42
|
+
registerAnalyticsHandler(pi, runtime, config);
|
|
40
43
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection.ts — PMA-1 connection manager for analytics.db.
|
|
3
|
+
*
|
|
4
|
+
* Own module-level connection cache, deliberately SEPARATE from the main
|
|
5
|
+
* memory store (openStore) and the turns store (openTurnDb) so an analytics-DB
|
|
6
|
+
* failure can never touch the authoritative memory DB or the turns DB (PMA §3.1).
|
|
7
|
+
*
|
|
8
|
+
* Mirrors src/store/turns/connection.ts. PREVENT-PI-004: node:sqlite in-process,
|
|
9
|
+
* no network. PREVENT-002: parameterized path binding.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { DatabaseSync as Db } from "node:sqlite";
|
|
14
|
+
import { initAnalyticsSchema } from "./schema.js";
|
|
15
|
+
export const ANALYTICS_DB_FILE = "analytics.db";
|
|
16
|
+
/** Resolve the analytics DB path, honoring the env override. */
|
|
17
|
+
export function analyticsDbPath(stateDir) {
|
|
18
|
+
const env = process.env.MEGACOMPACT_ANALYTICS_DB_PATH;
|
|
19
|
+
if (env && env.trim() !== "")
|
|
20
|
+
return env.trim();
|
|
21
|
+
return join(stateDir, ANALYTICS_DB_FILE);
|
|
22
|
+
}
|
|
23
|
+
// Module-private cache — separate from openStore's cache and turns' cache.
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
/** File size helper (0 for in-memory). */
|
|
26
|
+
function dbFileSize(dbPath) {
|
|
27
|
+
try {
|
|
28
|
+
return statSync(dbPath).size;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Open (or reuse from cache) the analytics.db for `stateDir`.
|
|
36
|
+
* Sets PRAGMAs, initializes the schema, and caches the connection.
|
|
37
|
+
* In-memory mode (`:memory:`) is supported for tests.
|
|
38
|
+
*/
|
|
39
|
+
export function openAnalyticsDb(stateDir, options) {
|
|
40
|
+
const inMemory = options?.inMemory === true;
|
|
41
|
+
const resolvedPath = inMemory ? ":memory:" : (options?.dbPath ?? analyticsDbPath(stateDir));
|
|
42
|
+
const cacheKey = inMemory ? `:memory:${stateDir}` : resolvedPath;
|
|
43
|
+
const cached = cache.get(cacheKey);
|
|
44
|
+
if (cached) {
|
|
45
|
+
try {
|
|
46
|
+
cached.exec("SELECT 1");
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Stale handle — evict and re-open.
|
|
51
|
+
cache.delete(cacheKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Ensure the state dir exists for file-backed mode.
|
|
55
|
+
if (!inMemory && !existsSync(stateDir)) {
|
|
56
|
+
mkdirSync(stateDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
const db = inMemory ? new Db(":memory:") : new Db(resolvedPath);
|
|
59
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
60
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
61
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
62
|
+
initAnalyticsSchema(db);
|
|
63
|
+
cache.set(cacheKey, db);
|
|
64
|
+
return db;
|
|
65
|
+
}
|
|
66
|
+
/** Close and evict the analytics DB for `stateDir` (idempotent). */
|
|
67
|
+
export function closeAnalyticsDb(stateDir, options) {
|
|
68
|
+
const inMemory = options?.inMemory === true;
|
|
69
|
+
const resolvedPath = inMemory ? ":memory:" : (options?.dbPath ?? analyticsDbPath(stateDir));
|
|
70
|
+
const cacheKey = inMemory ? `:memory:${stateDir}` : resolvedPath;
|
|
71
|
+
const db = cache.get(cacheKey);
|
|
72
|
+
if (!db)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
db.close();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* already closed */
|
|
79
|
+
}
|
|
80
|
+
cache.delete(cacheKey);
|
|
81
|
+
}
|
|
82
|
+
/** Close ALL cached analytics DBs (test teardown / graceful shutdown). */
|
|
83
|
+
export function closeAllAnalyticsDbs() {
|
|
84
|
+
for (const db of cache.values()) {
|
|
85
|
+
try {
|
|
86
|
+
db.close();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* already closed */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
cache.clear();
|
|
93
|
+
}
|
|
94
|
+
/** Report the file size of analytics.db for a state dir (0 if absent/in-memory). */
|
|
95
|
+
export function analyticsDbSize(stateDir) {
|
|
96
|
+
return dbFileSize(analyticsDbPath(stateDir));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* SAVEPOINT-based transaction wrapper (nests safely under an outer transaction).
|
|
100
|
+
* Uses a distinct savepoint name from the turns store (`turns_tx`) and the main
|
|
101
|
+
* store (`mc_tx`) to avoid collisions.
|
|
102
|
+
*/
|
|
103
|
+
export function withTx(db, fn) {
|
|
104
|
+
db.exec("SAVEPOINT analytics_tx");
|
|
105
|
+
try {
|
|
106
|
+
fn();
|
|
107
|
+
db.exec("RELEASE analytics_tx");
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
db.exec("ROLLBACK TO analytics_tx");
|
|
111
|
+
db.exec("RELEASE analytics_tx");
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let counter = 0;
|
|
2
|
+
/** Create a valid RequestEventFact with sensible defaults + overrides. */
|
|
3
|
+
export function makeRequestEvent(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
id: `evt_${++counter}`,
|
|
6
|
+
correlationId: "corr_test",
|
|
7
|
+
sessionId: "sess_test",
|
|
8
|
+
eventKind: "request_started",
|
|
9
|
+
observedAt: Date.now(),
|
|
10
|
+
provider: "anthropic",
|
|
11
|
+
model: "claude-sonnet-4",
|
|
12
|
+
source: "test",
|
|
13
|
+
quality: {},
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Create a valid MeasurementFact. */
|
|
18
|
+
export function makeMeasurement(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
observedAt: Date.now(),
|
|
21
|
+
sampleKind: "cache_hit_pct",
|
|
22
|
+
value: 72.5,
|
|
23
|
+
unit: "percent",
|
|
24
|
+
source: "test",
|
|
25
|
+
quality: {},
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Create a valid IdentityObservation. */
|
|
30
|
+
export function makeIdentity(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
observedAt: Date.now(),
|
|
33
|
+
provider: "anthropic",
|
|
34
|
+
model: "claude-sonnet-4",
|
|
35
|
+
source: "test",
|
|
36
|
+
metadata: { contextWindow: 200000 },
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — Barrel for src/store/analytics/ (PMA-1).
|
|
3
|
+
*
|
|
4
|
+
* Hosts import types + the factory. SQL schemas + internals stay private.
|
|
5
|
+
*
|
|
6
|
+
* PREVENT-PI-004: no network. PREVENT-002: parameterized SQL only.
|
|
7
|
+
*/
|
|
8
|
+
export { SqliteAnalyticsStore } from "./sqlite-store.js";
|
|
9
|
+
export { InMemoryAnalyticsStore } from "./memory-store.js";
|
|
10
|
+
export { openAnalyticsDb, closeAnalyticsDb, closeAllAnalyticsDbs, analyticsDbPath, analyticsDbSize, ANALYTICS_DB_FILE, withTx, } from "./connection.js";
|
|
11
|
+
export { initAnalyticsSchema, getAnalyticsSchemaVersion } from "./schema.js";
|
|
12
|
+
import { SqliteAnalyticsStore } from "./sqlite-store.js";
|
|
13
|
+
import { InMemoryAnalyticsStore } from "./memory-store.js";
|
|
14
|
+
/**
|
|
15
|
+
* Factory: create an AnalyticsStore from options.
|
|
16
|
+
*
|
|
17
|
+
* - Default: SqliteAnalyticsStore backed by analytics.db in stateDir
|
|
18
|
+
* - inMemory: InMemoryAnalyticsStore (no file I/O)
|
|
19
|
+
*/
|
|
20
|
+
export function createAnalyticsStore(options) {
|
|
21
|
+
if (options.inMemory) {
|
|
22
|
+
return new InMemoryAnalyticsStore(options);
|
|
23
|
+
}
|
|
24
|
+
return new SqliteAnalyticsStore(options);
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getAnalyticsSchemaVersion } from "./schema.js";
|
|
2
|
+
export class InMemoryAnalyticsStore {
|
|
3
|
+
events = new Map();
|
|
4
|
+
measurements = [];
|
|
5
|
+
identities = [];
|
|
6
|
+
nextId = 1;
|
|
7
|
+
constructor(_options) {
|
|
8
|
+
// Options unused — the in-memory store doesn't open a DB.
|
|
9
|
+
}
|
|
10
|
+
allocId() {
|
|
11
|
+
return this.nextId++;
|
|
12
|
+
}
|
|
13
|
+
// ── AnalyticsReader ───────────────────────────────────────────────
|
|
14
|
+
status() {
|
|
15
|
+
const lastEvent = this.events.size > 0
|
|
16
|
+
? Math.max(...[...this.events.values()].map((e) => e.observedAt))
|
|
17
|
+
: 0;
|
|
18
|
+
const lastMeas = this.measurements.length > 0
|
|
19
|
+
? Math.max(...this.measurements.map((m) => m.observedAt))
|
|
20
|
+
: 0;
|
|
21
|
+
const lastId = this.identities.length > 0
|
|
22
|
+
? Math.max(...this.identities.map((i) => i.observedAt))
|
|
23
|
+
: 0;
|
|
24
|
+
return {
|
|
25
|
+
enabled: true,
|
|
26
|
+
schemaVersion: getAnalyticsSchemaVersion(),
|
|
27
|
+
requestEventCount: this.events.size,
|
|
28
|
+
measurementCount: this.measurements.length,
|
|
29
|
+
identityCount: this.identities.length,
|
|
30
|
+
freshThrough: Math.max(lastEvent, lastMeas, lastId) || null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ── AnalyticsWriter ───────────────────────────────────────────────
|
|
34
|
+
appendRequestEvent(fact) {
|
|
35
|
+
if (this.events.has(fact.id)) {
|
|
36
|
+
return { status: "duplicate", id: fact.id };
|
|
37
|
+
}
|
|
38
|
+
this.events.set(fact.id, { ...fact });
|
|
39
|
+
return { status: "accepted", id: fact.id };
|
|
40
|
+
}
|
|
41
|
+
appendMeasurement(sample) {
|
|
42
|
+
const id = this.allocId();
|
|
43
|
+
this.measurements.push({ ...sample, id });
|
|
44
|
+
return { status: "accepted", id };
|
|
45
|
+
}
|
|
46
|
+
appendIdentity(observation) {
|
|
47
|
+
const id = this.allocId();
|
|
48
|
+
this.identities.push({ ...observation, id });
|
|
49
|
+
return { status: "accepted", id };
|
|
50
|
+
}
|
|
51
|
+
// ── AnalyticsAdmin ────────────────────────────────────────────────
|
|
52
|
+
prune(policy) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const eventCutoff = now - policy.maxEventAgeMs;
|
|
55
|
+
const measCutoff = now - policy.maxMeasurementAgeMs;
|
|
56
|
+
const beforeE = this.events.size;
|
|
57
|
+
const beforeM = this.measurements.length;
|
|
58
|
+
for (const [k, e] of this.events) {
|
|
59
|
+
if (e.observedAt < eventCutoff)
|
|
60
|
+
this.events.delete(k);
|
|
61
|
+
}
|
|
62
|
+
this.measurements = this.measurements.filter((m) => m.observedAt >= measCutoff);
|
|
63
|
+
return {
|
|
64
|
+
eventsRemoved: beforeE - this.events.size,
|
|
65
|
+
measurementsRemoved: beforeM - this.measurements.length,
|
|
66
|
+
identitiesRemoved: 0,
|
|
67
|
+
freedBytes: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
vacuum() {
|
|
71
|
+
return { ok: true, detail: "no-op for in-memory store" };
|
|
72
|
+
}
|
|
73
|
+
integrityCheck() {
|
|
74
|
+
return { ok: true, detail: "ok" };
|
|
75
|
+
}
|
|
76
|
+
backup() {
|
|
77
|
+
return { ok: false, detail: "Cannot backup an in-memory database" };
|
|
78
|
+
}
|
|
79
|
+
clear() {
|
|
80
|
+
this.events.clear();
|
|
81
|
+
this.measurements = [];
|
|
82
|
+
this.identities = [];
|
|
83
|
+
this.nextId = 1;
|
|
84
|
+
}
|
|
85
|
+
close() {
|
|
86
|
+
this.clear();
|
|
87
|
+
}
|
|
88
|
+
// ── Capability views ──────────────────────────────────────────────
|
|
89
|
+
asReader() {
|
|
90
|
+
return {
|
|
91
|
+
status: () => this.status(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
asWriter() {
|
|
95
|
+
return {
|
|
96
|
+
appendRequestEvent: (f) => this.appendRequestEvent(f),
|
|
97
|
+
appendMeasurement: (s) => this.appendMeasurement(s),
|
|
98
|
+
appendIdentity: (o) => this.appendIdentity(o),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
asAdmin() {
|
|
102
|
+
return {
|
|
103
|
+
prune: (p) => this.prune(p),
|
|
104
|
+
vacuum: () => this.vacuum(),
|
|
105
|
+
integrityCheck: () => this.integrityCheck(),
|
|
106
|
+
backup: () => this.backup(),
|
|
107
|
+
clear: () => this.clear(),
|
|
108
|
+
close: () => this.close(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const SCHEMA_VERSION = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Initialize the analytics.db schema. Safe to call on every open — every
|
|
4
|
+
* statement is CREATE IF NOT EXISTS.
|
|
5
|
+
*/
|
|
6
|
+
export function initAnalyticsSchema(db) {
|
|
7
|
+
// ── Lifecycle tables ──────────────────────────────────────────────
|
|
8
|
+
db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS analytics_meta (
|
|
10
|
+
key TEXT PRIMARY KEY,
|
|
11
|
+
value TEXT NOT NULL
|
|
12
|
+
)
|
|
13
|
+
`);
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS analytics_schema (
|
|
16
|
+
version INTEGER PRIMARY KEY,
|
|
17
|
+
applied_at INTEGER NOT NULL,
|
|
18
|
+
digest TEXT NOT NULL
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS analytics_migrations (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
started_at INTEGER NOT NULL,
|
|
25
|
+
completed_at INTEGER,
|
|
26
|
+
source_fingerprint TEXT,
|
|
27
|
+
rows_copied INTEGER,
|
|
28
|
+
status TEXT NOT NULL,
|
|
29
|
+
detail TEXT
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
// ── Append-only fact tables ───────────────────────────────────────
|
|
33
|
+
db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS request_events (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
correlation_id TEXT,
|
|
37
|
+
session_id TEXT,
|
|
38
|
+
repo_id TEXT,
|
|
39
|
+
turn_id TEXT,
|
|
40
|
+
event_kind TEXT NOT NULL CHECK(event_kind IN (
|
|
41
|
+
'request_started',
|
|
42
|
+
'provider_selected',
|
|
43
|
+
'first_token',
|
|
44
|
+
'request_completed',
|
|
45
|
+
'request_failed'
|
|
46
|
+
)),
|
|
47
|
+
observed_at INTEGER NOT NULL,
|
|
48
|
+
provider TEXT,
|
|
49
|
+
model TEXT,
|
|
50
|
+
status TEXT,
|
|
51
|
+
input_tokens INTEGER,
|
|
52
|
+
output_tokens INTEGER,
|
|
53
|
+
cache_read_tokens INTEGER,
|
|
54
|
+
cache_write_tokens INTEGER,
|
|
55
|
+
duration_ms REAL,
|
|
56
|
+
ttft_ms REAL,
|
|
57
|
+
source TEXT NOT NULL,
|
|
58
|
+
quality_json TEXT NOT NULL
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
db.exec(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS measurement_samples (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
observed_at INTEGER NOT NULL,
|
|
65
|
+
sample_kind TEXT NOT NULL,
|
|
66
|
+
provider TEXT,
|
|
67
|
+
model TEXT,
|
|
68
|
+
value REAL NOT NULL,
|
|
69
|
+
unit TEXT NOT NULL,
|
|
70
|
+
correlation_id TEXT,
|
|
71
|
+
source TEXT NOT NULL,
|
|
72
|
+
quality_json TEXT NOT NULL
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
db.exec(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS identity_observations (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
observed_at INTEGER NOT NULL,
|
|
79
|
+
provider TEXT,
|
|
80
|
+
model TEXT,
|
|
81
|
+
source TEXT NOT NULL,
|
|
82
|
+
metadata_json TEXT NOT NULL
|
|
83
|
+
)
|
|
84
|
+
`);
|
|
85
|
+
// ── Indexes (spec §6) ─────────────────────────────────────────────
|
|
86
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_observed_at ON request_events(observed_at)`);
|
|
87
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_provider_observed ON request_events(provider, observed_at)`);
|
|
88
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_model_observed ON request_events(model, observed_at)`);
|
|
89
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_kind_observed ON request_events(event_kind, observed_at)`);
|
|
90
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_correlation ON request_events(correlation_id, observed_at)`);
|
|
91
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_measurement_samples_kind_observed ON measurement_samples(sample_kind, observed_at)`);
|
|
92
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_measurement_samples_provider_model ON measurement_samples(provider, model, observed_at)`);
|
|
93
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_identity_obs_provider_model ON identity_observations(provider, model, observed_at)`);
|
|
94
|
+
// ── Schema version stamp (insert-only, never overwrites) ─────────
|
|
95
|
+
const existing = db
|
|
96
|
+
.prepare("SELECT value FROM analytics_meta WHERE key = 'schema_version'")
|
|
97
|
+
.get();
|
|
98
|
+
if (!existing) {
|
|
99
|
+
db.prepare("INSERT INTO analytics_meta (key, value) VALUES ('schema_version', ?)")
|
|
100
|
+
.run(String(SCHEMA_VERSION));
|
|
101
|
+
db.prepare("INSERT INTO analytics_schema (version, applied_at, digest) VALUES (?, ?, ?)")
|
|
102
|
+
.run(SCHEMA_VERSION, Date.now(), "pma-1-initial");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** The current analytics schema version. */
|
|
106
|
+
export function getAnalyticsSchemaVersion() {
|
|
107
|
+
return SCHEMA_VERSION;
|
|
108
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin.ts — AnalyticsAdmin delegate bodies for the SQLite backend.
|
|
3
|
+
* Destructive/maintenance operations: prune, vacuum, integrityCheck, backup, clear, close.
|
|
4
|
+
*/
|
|
5
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { withTx } from "../connection.js";
|
|
7
|
+
import { analyticsDbPath } from "../connection.js";
|
|
8
|
+
export function prune(ctx, policy) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const eventCutoff = now - policy.maxEventAgeMs;
|
|
11
|
+
const measCutoff = now - policy.maxMeasurementAgeMs;
|
|
12
|
+
const delEvents = ctx.db
|
|
13
|
+
.prepare("DELETE FROM request_events WHERE observed_at < ?")
|
|
14
|
+
.run(eventCutoff);
|
|
15
|
+
const delMeas = ctx.db
|
|
16
|
+
.prepare("DELETE FROM measurement_samples WHERE observed_at < ?")
|
|
17
|
+
.run(measCutoff);
|
|
18
|
+
// Identity observations are not age-pruned (they're low-volume snapshots).
|
|
19
|
+
if (policy.vacuumAfterPrune) {
|
|
20
|
+
try {
|
|
21
|
+
ctx.db.exec("VACUUM");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* non-fatal */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
eventsRemoved: delEvents.changes ?? 0,
|
|
29
|
+
measurementsRemoved: delMeas.changes ?? 0,
|
|
30
|
+
identitiesRemoved: 0,
|
|
31
|
+
freedBytes: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function vacuum(ctx) {
|
|
35
|
+
try {
|
|
36
|
+
ctx.db.exec("VACUUM");
|
|
37
|
+
return { ok: true, detail: "VACUUM completed" };
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return { ok: false, detail: String(e) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function integrityCheck(ctx) {
|
|
44
|
+
try {
|
|
45
|
+
const row = ctx.db.prepare("PRAGMA integrity_check").get();
|
|
46
|
+
const result = row?.integrity_check ?? "ok";
|
|
47
|
+
return { ok: result === "ok", detail: result };
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return { ok: false, detail: String(e) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function backup(ctx) {
|
|
54
|
+
if (ctx.opts.inMemory) {
|
|
55
|
+
return { ok: false, detail: "Cannot backup an in-memory database" };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
// WAL-safe checkpoint before copy.
|
|
59
|
+
ctx.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
60
|
+
const src = analyticsDbPath(ctx.stateDir);
|
|
61
|
+
if (!existsSync(src))
|
|
62
|
+
return { ok: false, detail: "analytics.db not found" };
|
|
63
|
+
const dest = `${src}.bak-${Date.now()}`;
|
|
64
|
+
copyFileSync(src, dest);
|
|
65
|
+
return { ok: true, path: dest, detail: `Backup written to ${dest}` };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return { ok: false, detail: String(e) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function clear(ctx) {
|
|
72
|
+
withTx(ctx.db, () => {
|
|
73
|
+
ctx.db.exec("DELETE FROM request_events");
|
|
74
|
+
ctx.db.exec("DELETE FROM measurement_samples");
|
|
75
|
+
ctx.db.exec("DELETE FROM identity_observations");
|
|
76
|
+
// Reset AUTOINCREMENT sequences for measurement_samples + identity_observations.
|
|
77
|
+
try {
|
|
78
|
+
ctx.db.exec("DELETE FROM sqlite_sequence WHERE name IN ('measurement_samples', 'identity_observations')");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* non-fatal: sqlite_sequence may not exist yet */
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|