pi-mega-compact 0.11.2 → 0.11.4
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/api-contracts/endpoints.js +9 -1
- package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/index-reader.js +22 -0
- package/dist/extensions/dashboard-server/routes-cache.js +80 -0
- package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
- package/dist/extensions/dashboard-server/routes-maintenance.js +7 -7
- package/dist/extensions/dashboard-server/routes-repo.js +24 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/extensions/mega-cache-replay.test.js +233 -24
- package/dist/extensions/mega-compact-s38.test.js +778 -20
- package/dist/extensions/mega-config.js +4 -0
- package/dist/extensions/mega-events/agent-handlers.js +44 -8
- package/dist/extensions/mega-events/context-handler.js +37 -19
- package/dist/extensions/mega-events/error-classifier.js +125 -135
- package/dist/extensions/mega-events/outage-advisor.js +53 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
- package/dist/extensions/mega-runtime/runtime.js +1 -0
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
- package/dist/extensions/mega-runtime/widget.js +4 -4
- package/dist/extensions/mega-runtime/widget.test.js +103 -12
- package/dist/src/pricing.js +78 -0
- package/dist/src/store/sqlite/perf-samples.js +104 -0
- package/dist/src/store/sqlite/perf-samples.test.js +93 -1
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js → AchievementTiles-vEo2MtE2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B8NqDt7I.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js → ConfigTab-Cm6ymbmx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js → EventsTab-DXWl_YzI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js → GameTab-BLDudcO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js.map → GameTab-BLDudcO5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js → MetricsTab-Cq4u-rZF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js → OverviewTab-D4kGvsiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js → SessionsTab-DZC2-GCn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js → TopicsTab-01G71hv1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js.map → TopicsTab-01G71hv1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js → TurnsTab-DLRg0LZT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js → index-BX0bcgLu.js} +4 -4
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js.map → index-BX0bcgLu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js → useSSE-BBYt5UPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +6 -0
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
- package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
- package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +301 -136
- package/extensions/dashboard-client/src/utils/format.ts +42 -7
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -8
- package/extensions/dashboard-server/api-contracts/index.ts +4 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
- package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
- package/extensions/dashboard-server/api-contracts.test.ts +3 -2
- package/extensions/dashboard-server/index-reader.ts +23 -0
- package/extensions/dashboard-server/routes-cache.test.ts +302 -0
- package/extensions/dashboard-server/routes-cache.ts +123 -0
- package/extensions/dashboard-server/routes-maintenance.ts +18 -11
- package/extensions/dashboard-server/routes-repo.ts +31 -4
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/extensions/mega-cache-replay.test.ts +481 -166
- package/extensions/mega-compact-s38.test.ts +785 -20
- package/extensions/mega-config.ts +14 -0
- package/extensions/mega-events/agent-handlers.ts +48 -7
- package/extensions/mega-events/context-handler.ts +59 -24
- package/extensions/mega-events/error-classifier.ts +167 -129
- package/extensions/mega-events/outage-advisor.ts +73 -0
- package/extensions/mega-runtime/helpers.ts +2 -0
- package/extensions/mega-runtime/reset-runtime.ts +1 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
- package/extensions/mega-runtime/runtime.ts +1 -0
- package/extensions/mega-runtime/snapshot.test.ts +139 -0
- package/extensions/mega-runtime/snapshot.ts +6 -4
- package/extensions/mega-runtime/widget.test.ts +290 -105
- package/extensions/mega-runtime/widget.ts +29 -6
- package/package.json +1 -1
- package/src/pricing.ts +95 -0
- package/src/store/sqlite/perf-samples.test.ts +107 -3
- package/src/store/sqlite/perf-samples.ts +142 -6
- package/extensions/dashboard-client/dist/assets/AchievementsTab-B8NqDt7I.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Sprint A1 — PREVENT-PI-004: zero network code (type definitions only).
|
|
9
9
|
* PREVENT-011: no `any` type — all types are explicit.
|
|
10
10
|
*/
|
|
11
|
+
// ─── Provider Cache Lifetime (A.2) — see provider-cache.ts ─────────────────
|
|
11
12
|
// ─── Game Score & Achievement Types ─────────────────────────────────────────
|
|
12
13
|
// (moved to game-types.ts; re-exported for backwards compatibility)
|
|
13
14
|
// ─── Sessions Memory Graph (S39) ─────────────────────────────────────────
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
// (moved to game-types.ts; re-exported for backwards compatibility)
|
|
19
20
|
// ─── ENDPOINTS Registry ─────────────────────────────────────────────────────
|
|
20
21
|
/**
|
|
21
|
-
* Central registry of all
|
|
22
|
+
* Central registry of all 14 dashboard API endpoints. Each entry is an
|
|
22
23
|
* `EndpointDef` or `SseEndpointDef` instance serving as the single source of
|
|
23
24
|
* truth for route paths, methods, descriptions, and typed request/response
|
|
24
25
|
* contracts.
|
|
@@ -188,4 +189,11 @@ export const ENDPOINTS = {
|
|
|
188
189
|
path: "/api/maintenance/action",
|
|
189
190
|
description: "Run vacuum, checkpoint, reindex, fts5-rebuild, reconcile-dedup, prune, or integrity-check.",
|
|
190
191
|
},
|
|
192
|
+
// ─── Provider Cache (Sprint A.2) ─────────────────────────────────
|
|
193
|
+
/** GET /api/provider-cache — Lifetime provider prompt cache aggregates + $ savings. */
|
|
194
|
+
providerCache: {
|
|
195
|
+
method: "GET",
|
|
196
|
+
path: "/api/provider-cache",
|
|
197
|
+
description: "Lifetime provider prompt cache hit-rate aggregates + dollar savings estimate.",
|
|
198
|
+
},
|
|
191
199
|
};
|
|
@@ -76,7 +76,7 @@ void _c_achievements;
|
|
|
76
76
|
// Verify the ENDPOINTS registry has exactly 15 entries with correct paths.
|
|
77
77
|
// (Sprint 39 added /api/sessions + /api/sessions/timeseries.)
|
|
78
78
|
const ENDPOINT_KEYS = Object.keys(ENDPOINTS);
|
|
79
|
-
const EXPECTED_ENDPOINT_COUNT =
|
|
79
|
+
const EXPECTED_ENDPOINT_COUNT = 27; // 23 base + 3 S49B maintenance + 1 provider-cache
|
|
80
80
|
/** All `/api/*` paths served by server.ts (extracted from the route handlers). */
|
|
81
81
|
const SERVER_TS_PATHS = [
|
|
82
82
|
"/api/snapshot",
|
|
@@ -94,10 +94,11 @@ const SERVER_TS_PATHS = [
|
|
|
94
94
|
"/api/sessions",
|
|
95
95
|
"/api/sessions/timeseries",
|
|
96
96
|
"/api/topics",
|
|
97
|
+
"/api/provider-cache",
|
|
97
98
|
];
|
|
98
99
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
99
100
|
describe("ENDPOINTS registry", () => {
|
|
100
|
-
test("has exactly
|
|
101
|
+
test("has exactly the expected endpoint count", () => {
|
|
101
102
|
assert.equal(ENDPOINT_KEYS.length, EXPECTED_ENDPOINT_COUNT, `ENDPOINTS must have ${EXPECTED_ENDPOINT_COUNT} entries, got ${ENDPOINT_KEYS.length}`);
|
|
102
103
|
});
|
|
103
104
|
test("every server.ts /api/* path has a corresponding ENDPOINTS entry", () => {
|
|
@@ -57,6 +57,9 @@ export function readIndex() {
|
|
|
57
57
|
contextWindow: null,
|
|
58
58
|
maxTokens: null,
|
|
59
59
|
reasoning: null,
|
|
60
|
+
providerCachePct: null,
|
|
61
|
+
providerCacheRead: null,
|
|
62
|
+
providerCacheWrite: null,
|
|
60
63
|
}));
|
|
61
64
|
// Enrich each repo with per-store token + model detail read directly via
|
|
62
65
|
// node:sqlite (same zero-dependency invariant as readIndex; no store graph
|
|
@@ -86,6 +89,25 @@ export function readIndex() {
|
|
|
86
89
|
repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
|
|
87
90
|
repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
|
|
88
91
|
}
|
|
92
|
+
// Provider prompt-cache stats (E.3)
|
|
93
|
+
try {
|
|
94
|
+
const pRow = sdb
|
|
95
|
+
.prepare(`SELECT
|
|
96
|
+
SUM(cache_read) AS totalRead,
|
|
97
|
+
SUM(cache_write) AS totalWrite,
|
|
98
|
+
SUM(cache_read + cache_write) * 1.0 / MAX(SUM(cache_read + cache_write + tokens_in), 1) AS hitPct
|
|
99
|
+
FROM perf_samples
|
|
100
|
+
WHERE cache_read > 0 OR cache_write > 0`)
|
|
101
|
+
.get();
|
|
102
|
+
if (pRow && pRow.totalRead > 0) {
|
|
103
|
+
repo.providerCacheRead = Number(pRow.totalRead);
|
|
104
|
+
repo.providerCacheWrite = Number(pRow.totalWrite);
|
|
105
|
+
repo.providerCachePct = Number((pRow.hitPct * 100).toFixed(1));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* best-effort — perf_samples may not exist */
|
|
110
|
+
}
|
|
89
111
|
}
|
|
90
112
|
finally {
|
|
91
113
|
sdb.close();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-cache.ts — Provider prompt-cache stats route handler.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/provider-cache — Lifetime provider prompt cache hit-rate aggregates
|
|
5
|
+
* plus dollar savings estimate (priced from latest model snapshot).
|
|
6
|
+
*
|
|
7
|
+
* Guardrails: PREVENT-PI-004 (loopback-only), PREVENT-001 (null-safe JSON parse),
|
|
8
|
+
* PREVENT-002 (parameterized SQL). Loopback-only localhost dashboard endpoint.
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { computeCacheSavings } from "../../src/pricing.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// handleProviderCache — "/api/provider-cache"
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export function handleProviderCache(req, res, ctx) {
|
|
16
|
+
if (!req.url?.startsWith("/api/provider-cache"))
|
|
17
|
+
return false;
|
|
18
|
+
const { stateDir } = ctx;
|
|
19
|
+
// /api/provider-cache — GET returns lifetime provider prompt cache aggregates
|
|
20
|
+
// from perf_samples (cache_hit_pct rows with meta JSON) plus an optional
|
|
21
|
+
// dollar savings estimate priced from the latest model snapshot. The dashboard
|
|
22
|
+
// server is a detached child with no MegaRuntime ref, so it reads via a
|
|
23
|
+
// require()'d sqlite helper (same pattern as /api/perf). Non-GET → 405.
|
|
24
|
+
// PREVENT-PI-004: loopback.
|
|
25
|
+
const pfReq = createRequire(import.meta.url);
|
|
26
|
+
const { readProviderCacheLifetime, latestModelSnapshot } = pfReq("../../src/store/sqlite.js");
|
|
27
|
+
if (req.method !== "GET") {
|
|
28
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
29
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
30
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const lifetime = readProviderCacheLifetime(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
35
|
+
// D3: compute dollar savings from the latest model snapshot. Null-safe:
|
|
36
|
+
// when no snapshot or zero input rate, savings stays null (UI shows "—").
|
|
37
|
+
let savings = null;
|
|
38
|
+
try {
|
|
39
|
+
const snap = latestModelSnapshot(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
40
|
+
if (snap && snap.inputRate > 0) {
|
|
41
|
+
const s = computeCacheSavings(lifetime.totalCacheRead, lifetime.totalCacheWrite, snap.inputRate);
|
|
42
|
+
savings = {
|
|
43
|
+
cacheReadSaved: s.cacheReadSaved,
|
|
44
|
+
cacheWriteCost: s.cacheWriteCost,
|
|
45
|
+
netSaved: s.netSaved,
|
|
46
|
+
model: snap.modelName ?? snap.modelId ?? "unknown",
|
|
47
|
+
inputRate: snap.inputRate,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* pricing unavailable — savings stays null */
|
|
53
|
+
}
|
|
54
|
+
const body = {
|
|
55
|
+
cache: {
|
|
56
|
+
avgHitPct: lifetime.avgHitPct,
|
|
57
|
+
turnCount: lifetime.sampleCount,
|
|
58
|
+
totalCacheRead: lifetime.totalCacheRead,
|
|
59
|
+
totalCacheWrite: lifetime.totalCacheWrite,
|
|
60
|
+
totalInput: lifetime.totalInput,
|
|
61
|
+
firstTurnAt: lifetime.firstSampleAt,
|
|
62
|
+
latestTurnAt: lifetime.latestSampleAt,
|
|
63
|
+
},
|
|
64
|
+
savings,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
68
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
69
|
+
res.end(JSON.stringify(body));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
73
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
74
|
+
res.end(JSON.stringify({
|
|
75
|
+
error: "provider_cache_unavailable",
|
|
76
|
+
detail: String(e),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-cache.test.ts — Sprint A /api/provider-cache endpoint tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers the A.4 spec:
|
|
5
|
+
* - GET 200 → shape matches ProviderCacheResponse
|
|
6
|
+
* - POST → 405
|
|
7
|
+
* - empty perf_samples → zeros/nulls
|
|
8
|
+
* - no model snapshot → savings: null
|
|
9
|
+
* - with model snapshot → priced savings fields
|
|
10
|
+
*
|
|
11
|
+
* Uses the same spawn-and-fetch harness as perf-server.test.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { test, describe } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { recordPerfSample, recordModelSnapshot, } from "../../src/store/sqlite.js";
|
|
20
|
+
const SERVER_ENTRY = new URL("./server.js", import.meta.url).pathname;
|
|
21
|
+
function waitFor(cond, timeoutMs = 6000) {
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const tick = async () => {
|
|
25
|
+
if (await cond())
|
|
26
|
+
return resolve();
|
|
27
|
+
if (Date.now() - start > timeoutMs)
|
|
28
|
+
return reject(new Error("timeout"));
|
|
29
|
+
setTimeout(tick, 50);
|
|
30
|
+
};
|
|
31
|
+
tick();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function freshDir(prefix) {
|
|
35
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
36
|
+
}
|
|
37
|
+
async function withServer(port, dir, fn) {
|
|
38
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = port;
|
|
39
|
+
const child = spawn(process.execPath, [SERVER_ENTRY, dir], {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
await waitFor(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
46
|
+
const res = await fetch(`http://localhost:${raw.port}/api/version`);
|
|
47
|
+
return res.ok;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
54
|
+
return await fn(raw.port);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
child.kill("SIGTERM");
|
|
58
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
describe("/api/provider-cache", () => {
|
|
63
|
+
test("GET 200 — shape matches ProviderCacheResponse", async () => {
|
|
64
|
+
const dir = freshDir("dash-pcache-agg-");
|
|
65
|
+
// Seed one cache_hit_pct sample with full meta.
|
|
66
|
+
recordPerfSample(dir, "cache_hit_pct", 42, {
|
|
67
|
+
cacheRead: 100,
|
|
68
|
+
cacheWrite: 20,
|
|
69
|
+
input: 500,
|
|
70
|
+
});
|
|
71
|
+
await withServer("19450", dir, async (port) => {
|
|
72
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
73
|
+
assert.equal(res.status, 200);
|
|
74
|
+
const body = (await res.json());
|
|
75
|
+
// Shape assertions
|
|
76
|
+
assert.ok("cache" in body, "should have cache");
|
|
77
|
+
assert.ok("savings" in body, "should have savings");
|
|
78
|
+
assert.ok("updatedAt" in body, "should have updatedAt");
|
|
79
|
+
assert.equal(body.cache.turnCount, 1);
|
|
80
|
+
assert.equal(body.cache.totalCacheRead, 100);
|
|
81
|
+
assert.equal(body.cache.totalCacheWrite, 20);
|
|
82
|
+
assert.equal(body.cache.totalInput, 500);
|
|
83
|
+
assert.ok(typeof body.cache.firstTurnAt === "string");
|
|
84
|
+
assert.ok(typeof body.cache.latestTurnAt === "string");
|
|
85
|
+
assert.ok(typeof body.updatedAt === "string");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
test("GET 200 — empty perf_samples returns zeros/nulls", async () => {
|
|
89
|
+
const dir = freshDir("dash-pcache-empty-");
|
|
90
|
+
await withServer("19451", dir, async (port) => {
|
|
91
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
92
|
+
assert.equal(res.status, 200);
|
|
93
|
+
const body = (await res.json());
|
|
94
|
+
assert.equal(body.cache.turnCount, 0);
|
|
95
|
+
assert.equal(body.cache.avgHitPct, 0);
|
|
96
|
+
assert.equal(body.cache.totalCacheRead, 0);
|
|
97
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
98
|
+
assert.equal(body.cache.totalInput, 0);
|
|
99
|
+
assert.equal(body.cache.firstTurnAt, null);
|
|
100
|
+
assert.equal(body.cache.latestTurnAt, null);
|
|
101
|
+
assert.equal(body.savings, null);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
test("POST → 405", async () => {
|
|
105
|
+
const dir = freshDir("dash-pcache-meth-");
|
|
106
|
+
await withServer("19452", dir, async (port) => {
|
|
107
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
});
|
|
110
|
+
assert.equal(res.status, 405);
|
|
111
|
+
const body = (await res.json());
|
|
112
|
+
assert.equal(body.error, "method_not_allowed");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test("no model snapshot → savings: null", async () => {
|
|
116
|
+
const dir = freshDir("dash-pcache-nosnap-");
|
|
117
|
+
recordPerfSample(dir, "cache_hit_pct", 50, {
|
|
118
|
+
cacheRead: 200,
|
|
119
|
+
cacheWrite: 30,
|
|
120
|
+
input: 1000,
|
|
121
|
+
});
|
|
122
|
+
await withServer("19453", dir, async (port) => {
|
|
123
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
124
|
+
assert.equal(res.status, 200);
|
|
125
|
+
const body = (await res.json());
|
|
126
|
+
assert.equal(body.savings, null);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
test("with model snapshot → priced savings fields", async () => {
|
|
130
|
+
const dir = freshDir("dash-pcache-savings-");
|
|
131
|
+
recordPerfSample(dir, "cache_hit_pct", 50, {
|
|
132
|
+
cacheRead: 2000,
|
|
133
|
+
cacheWrite: 400,
|
|
134
|
+
input: 10000,
|
|
135
|
+
});
|
|
136
|
+
// Record a model snapshot with a known input rate.
|
|
137
|
+
recordModelSnapshot("/tmp/test-repo", {
|
|
138
|
+
provider: "anthropic",
|
|
139
|
+
providerName: "Anthropic",
|
|
140
|
+
modelId: "claude-sonnet-4-20250514",
|
|
141
|
+
modelName: "Claude Sonnet 4",
|
|
142
|
+
inputRate: 3e-6, // $3 / 1M tokens
|
|
143
|
+
outputRate: 15e-6,
|
|
144
|
+
contextWindow: 200000,
|
|
145
|
+
maxTokens: 32000,
|
|
146
|
+
reasoning: false,
|
|
147
|
+
}, dir);
|
|
148
|
+
await withServer("19454", dir, async (port) => {
|
|
149
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
150
|
+
assert.equal(res.status, 200);
|
|
151
|
+
const body = (await res.json());
|
|
152
|
+
assert.ok(body.savings, "savings should not be null");
|
|
153
|
+
// cacheReadSaved = 2000 * 3e-6 * 0.9 = 0.0054
|
|
154
|
+
assert.ok(body.savings.cacheReadSaved > 0, "cacheReadSaved > 0");
|
|
155
|
+
// cacheWriteCost = 400 * 3e-6 * 0.25 = 0.0003
|
|
156
|
+
assert.ok(body.savings.cacheWriteCost > 0, "cacheWriteCost > 0");
|
|
157
|
+
assert.ok(body.savings.netSaved ===
|
|
158
|
+
body.savings.cacheReadSaved - body.savings.cacheWriteCost, "netSaved = cacheReadSaved - cacheWriteCost");
|
|
159
|
+
assert.equal(body.savings.model, "Claude Sonnet 4");
|
|
160
|
+
assert.equal(body.savings.inputRate, 3e-6);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
test("partial meta (missing fields) → treated as 0", async () => {
|
|
164
|
+
const dir = freshDir("dash-pcache-partial-");
|
|
165
|
+
// Only input in meta; cacheRead/cacheWrite missing → treated as 0.
|
|
166
|
+
recordPerfSample(dir, "cache_hit_pct", 30, { input: 800 });
|
|
167
|
+
// Only cacheRead in meta; input/cacheWrite missing → treated as 0.
|
|
168
|
+
recordPerfSample(dir, "cache_hit_pct", 60, { cacheRead: 150 });
|
|
169
|
+
await withServer("19455", dir, async (port) => {
|
|
170
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
171
|
+
assert.equal(res.status, 200);
|
|
172
|
+
const body = (await res.json());
|
|
173
|
+
// First sample: input=800, cacheRead=0, cacheWrite=0
|
|
174
|
+
// Second sample: cacheRead=150, input=0, cacheWrite=0
|
|
175
|
+
assert.equal(body.cache.turnCount, 2);
|
|
176
|
+
assert.equal(body.cache.totalCacheRead, 150);
|
|
177
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
178
|
+
assert.equal(body.cache.totalInput, 800);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
test("NULL meta → counted in turnCount, contributes 0 tokens + 0 avg", async () => {
|
|
182
|
+
const dir = freshDir("dash-pcache-nullmeta-");
|
|
183
|
+
// avgHitPct is derived from (cacheRead / (cacheRead + input + cacheWrite)) * 100,
|
|
184
|
+
// NOT from the `value` column. NULL meta → no tokens → avg 0, not 25.
|
|
185
|
+
recordPerfSample(dir, "cache_hit_pct", 25, undefined);
|
|
186
|
+
await withServer("19456", dir, async (port) => {
|
|
187
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
188
|
+
assert.equal(res.status, 200);
|
|
189
|
+
const body = (await res.json());
|
|
190
|
+
assert.equal(body.cache.turnCount, 1);
|
|
191
|
+
assert.equal(body.cache.avgHitPct, 0);
|
|
192
|
+
assert.equal(body.cache.totalCacheRead, 0);
|
|
193
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
194
|
+
assert.equal(body.cache.totalInput, 0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
test("model snapshot with modelName=null → falls back to modelId", async () => {
|
|
198
|
+
const dir = freshDir("dash-pcache-modelid-");
|
|
199
|
+
recordPerfSample(dir, "cache_hit_pct", 10, {
|
|
200
|
+
cacheRead: 100,
|
|
201
|
+
cacheWrite: 10,
|
|
202
|
+
input: 1000,
|
|
203
|
+
});
|
|
204
|
+
recordModelSnapshot("/tmp/test-repo", {
|
|
205
|
+
provider: "openai",
|
|
206
|
+
providerName: null,
|
|
207
|
+
modelId: "gpt-4o",
|
|
208
|
+
modelName: null,
|
|
209
|
+
inputRate: 2.5e-6,
|
|
210
|
+
outputRate: 10e-6,
|
|
211
|
+
contextWindow: 128000,
|
|
212
|
+
maxTokens: 16384,
|
|
213
|
+
reasoning: false,
|
|
214
|
+
}, dir);
|
|
215
|
+
await withServer("19457", dir, async (port) => {
|
|
216
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
217
|
+
assert.equal(res.status, 200);
|
|
218
|
+
const body = (await res.json());
|
|
219
|
+
assert.ok(body.savings);
|
|
220
|
+
assert.equal(body.savings.model, "gpt-4o");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
test("model snapshot with zero inputRate → savings: null", async () => {
|
|
224
|
+
const dir = freshDir("dash-pcache-zerorate-");
|
|
225
|
+
recordPerfSample(dir, "cache_hit_pct", 10, {
|
|
226
|
+
cacheRead: 100,
|
|
227
|
+
cacheWrite: 10,
|
|
228
|
+
input: 1000,
|
|
229
|
+
});
|
|
230
|
+
recordModelSnapshot("/tmp/test-repo", {
|
|
231
|
+
provider: "local",
|
|
232
|
+
providerName: null,
|
|
233
|
+
modelId: "local-llm",
|
|
234
|
+
modelName: null,
|
|
235
|
+
inputRate: 0,
|
|
236
|
+
outputRate: 0,
|
|
237
|
+
contextWindow: 4096,
|
|
238
|
+
maxTokens: 2048,
|
|
239
|
+
reasoning: false,
|
|
240
|
+
}, dir);
|
|
241
|
+
await withServer("19458", dir, async (port) => {
|
|
242
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
243
|
+
assert.equal(res.status, 200);
|
|
244
|
+
const body = (await res.json());
|
|
245
|
+
assert.equal(body.savings, null);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -102,14 +102,16 @@ function sendSchemaHealth(res, ctx) {
|
|
|
102
102
|
.get("schema_version");
|
|
103
103
|
const schemaVersion = metaRow ? Number(metaRow.value) : 0;
|
|
104
104
|
// PRAGMA foreign_key_check
|
|
105
|
-
const fkRows = db
|
|
106
|
-
.prepare("PRAGMA foreign_key_check")
|
|
107
|
-
.all();
|
|
105
|
+
const fkRows = db.prepare("PRAGMA foreign_key_check").all();
|
|
108
106
|
const fkCheck = fkRows.map((r) => JSON.stringify(r));
|
|
109
107
|
// Per-column audit: every ensureColumn migration column declared in
|
|
110
108
|
// schema.ts, checked via PRAGMA table_info (read-only).
|
|
111
109
|
const expectedColumns = [
|
|
112
|
-
{
|
|
110
|
+
{
|
|
111
|
+
table: "context_chunks",
|
|
112
|
+
column: "original_token_estimate",
|
|
113
|
+
decl: "INTEGER",
|
|
114
|
+
},
|
|
113
115
|
{ table: "raw_transcript", column: "content_ref", decl: "TEXT" },
|
|
114
116
|
{ table: "memories", column: "category", decl: "TEXT" },
|
|
115
117
|
{ table: "memories", column: "target", decl: "TEXT" },
|
|
@@ -121,9 +123,7 @@ function sendSchemaHealth(res, ctx) {
|
|
|
121
123
|
{ table: "session_state", column: "last_turn_id", decl: "INTEGER" },
|
|
122
124
|
];
|
|
123
125
|
const columns = expectedColumns.map((ec) => {
|
|
124
|
-
const cols = db
|
|
125
|
-
.prepare(`PRAGMA table_info(${ec.table})`)
|
|
126
|
-
.all();
|
|
126
|
+
const cols = db.prepare(`PRAGMA table_info(${ec.table})`).all();
|
|
127
127
|
const present = cols.some((c) => c.name === ec.column);
|
|
128
128
|
return {
|
|
129
129
|
table: ec.table,
|
|
@@ -7,6 +7,9 @@ import { readSnapshot } from "./snapshot.js";
|
|
|
7
7
|
import { dashboardHtml } from "./html.js";
|
|
8
8
|
import { readIndex, getIndexDir } from "./index-reader.js";
|
|
9
9
|
import { ACTIVE_WINDOW_SEC } from "./types.js";
|
|
10
|
+
import { readProviderCacheForRepo } from "../../src/store/sqlite/perf-samples.js";
|
|
11
|
+
import { computeCacheSavings, lookupModelInputRate } from "../../src/pricing.js";
|
|
12
|
+
import { latestModelSnapshot } from "../../src/store/sqlite/model-snapshots.js";
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// handleIndex — "/" | "/index.html" | "/api/snapshot" | "/api/version"
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
@@ -132,6 +135,27 @@ export function handleRepoIndex(req, res, ctx) {
|
|
|
132
135
|
out.compacts = snap.compacts ?? null;
|
|
133
136
|
out.timeSaved = snap.timeSaved ?? null;
|
|
134
137
|
out.updatedAt = snap.updatedAt ?? null;
|
|
138
|
+
// Provider cache per-repo lifetime (E.2)
|
|
139
|
+
try {
|
|
140
|
+
const pc = readProviderCacheForRepo(r.stateDir);
|
|
141
|
+
// Price from model snapshot if available
|
|
142
|
+
const modelSnap = latestModelSnapshot(r.stateDir);
|
|
143
|
+
const inputRate = modelSnap
|
|
144
|
+
? modelSnap.inputRate
|
|
145
|
+
: lookupModelInputRate(r.modelName ?? "") ?? 0;
|
|
146
|
+
const savings = computeCacheSavings(pc.totalCacheRead, pc.totalCacheWrite, inputRate);
|
|
147
|
+
out.providerCache = {
|
|
148
|
+
avgHitPct: pc.avgHitPct,
|
|
149
|
+
cacheRead: pc.totalCacheRead,
|
|
150
|
+
cacheWrite: pc.totalCacheWrite,
|
|
151
|
+
totalInput: pc.totalInput,
|
|
152
|
+
sampleCount: pc.sampleCount,
|
|
153
|
+
estimatedSaved: savings.netSaved > 0 ? savings.netSaved : null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* best-effort — repo may not have perf_samples */
|
|
158
|
+
}
|
|
135
159
|
}
|
|
136
160
|
}
|
|
137
161
|
catch {
|
|
@@ -11,3 +11,4 @@ export { handleEvents, handleSessions } from "./routes-sessions.js";
|
|
|
11
11
|
export { handleTopics } from "./routes-topics.js";
|
|
12
12
|
export { handleTurns } from "./routes-turns.js";
|
|
13
13
|
export { handleMaintenance } from "./routes-maintenance.js";
|
|
14
|
+
export { handleProviderCache } from "./routes-cache.js";
|
|
@@ -12,7 +12,7 @@ import { join, dirname } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { log, setLogPath, setDashboardServerVersion } from "./state.js";
|
|
15
|
-
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleStatic, } from "./routes.js";
|
|
15
|
+
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleStatic, } from "./routes.js";
|
|
16
16
|
export async function launchDashboardServer(stateDir) {
|
|
17
17
|
// Our own package version — exposed at /api/version so the launcher can
|
|
18
18
|
// detect a stale server (started by an older build) and replace it on
|
|
@@ -193,6 +193,8 @@ export async function launchDashboardServer(stateDir) {
|
|
|
193
193
|
return;
|
|
194
194
|
if (handleMaintenance(req, res, ctx))
|
|
195
195
|
return;
|
|
196
|
+
if (handleProviderCache(req, res, ctx))
|
|
197
|
+
return;
|
|
196
198
|
handleStatic(req, res, ctx);
|
|
197
199
|
});
|
|
198
200
|
// Bind base + range are env-configurable so tests can use a private,
|