pi-mega-compact 0.11.2 → 0.11.3
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 +201 -6
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +23 -7
- package/dist/extensions/mega-events/context-handler.js +37 -19
- package/dist/extensions/mega-events/error-classifier.js +27 -16
- package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
- 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 +210 -6
- package/extensions/mega-config.ts +6 -0
- package/extensions/mega-events/agent-handlers.ts +24 -6
- package/extensions/mega-events/context-handler.ts +59 -24
- package/extensions/mega-events/error-classifier.ts +30 -13
- package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
- 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
|
@@ -14,6 +14,7 @@ import type { SnapshotResponse } from "./snapshot.js";
|
|
|
14
14
|
import type { IndexesIndexRow, IndexesSummaryResponse } from "./multi-repo.js";
|
|
15
15
|
import type { GameStateResponse } from "./game.js";
|
|
16
16
|
import type { SseEvent } from "./index.js";
|
|
17
|
+
import type { ProviderCacheResponse } from "./provider-cache.js";
|
|
17
18
|
import type {
|
|
18
19
|
TurnsResponse,
|
|
19
20
|
ConversationTurnsResponse,
|
|
@@ -26,6 +27,13 @@ import type {
|
|
|
26
27
|
TopicMemoriesResponse,
|
|
27
28
|
} from "./turns.js";
|
|
28
29
|
|
|
30
|
+
import type {
|
|
31
|
+
DbStatsResponse,
|
|
32
|
+
MaintenanceActionResult,
|
|
33
|
+
MaintenanceAction,
|
|
34
|
+
SchemaHealthResponse,
|
|
35
|
+
} from "./maintenance.js";
|
|
36
|
+
|
|
29
37
|
import type {
|
|
30
38
|
GameScoreRow,
|
|
31
39
|
GameScoresQuery,
|
|
@@ -56,13 +64,6 @@ export type {
|
|
|
56
64
|
TopicDetailResponse,
|
|
57
65
|
} from "./game-types.js";
|
|
58
66
|
|
|
59
|
-
import type {
|
|
60
|
-
DbStatsResponse,
|
|
61
|
-
MaintenanceActionResult,
|
|
62
|
-
MaintenanceAction,
|
|
63
|
-
SchemaHealthResponse,
|
|
64
|
-
} from "./maintenance.js";
|
|
65
|
-
|
|
66
67
|
// ─── New Response Types (inline) ───────────────────────────────────────────
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -247,6 +248,15 @@ export interface ServerEntry {
|
|
|
247
248
|
} | null;
|
|
248
249
|
/** ISO timestamp of the live snapshot, or null. Present when a live snapshot has updatedAt. */
|
|
249
250
|
readonly updatedAt?: string | null;
|
|
251
|
+
/** Provider prompt-cache lifetime aggregates for this repo, or null when unavailable. */
|
|
252
|
+
readonly providerCache?: {
|
|
253
|
+
readonly avgHitPct: number;
|
|
254
|
+
readonly cacheRead: number;
|
|
255
|
+
readonly cacheWrite: number;
|
|
256
|
+
readonly totalInput: number;
|
|
257
|
+
readonly sampleCount: number;
|
|
258
|
+
readonly estimatedSaved: number | null;
|
|
259
|
+
} | null;
|
|
250
260
|
}
|
|
251
261
|
|
|
252
262
|
/**
|
|
@@ -352,6 +362,8 @@ export interface PerfResponse {
|
|
|
352
362
|
readonly diag: PerfDiag | null;
|
|
353
363
|
}
|
|
354
364
|
|
|
365
|
+
// ─── Provider Cache Lifetime (A.2) — see provider-cache.ts ─────────────────
|
|
366
|
+
|
|
355
367
|
// ─── Game Score & Achievement Types ─────────────────────────────────────────
|
|
356
368
|
// (moved to game-types.ts; re-exported for backwards compatibility)
|
|
357
369
|
|
|
@@ -367,7 +379,7 @@ export interface PerfResponse {
|
|
|
367
379
|
// ─── ENDPOINTS Registry ─────────────────────────────────────────────────────
|
|
368
380
|
|
|
369
381
|
/**
|
|
370
|
-
* Central registry of all
|
|
382
|
+
* Central registry of all 14 dashboard API endpoints. Each entry is an
|
|
371
383
|
* `EndpointDef` or `SseEndpointDef` instance serving as the single source of
|
|
372
384
|
* truth for route paths, methods, descriptions, and typed request/response
|
|
373
385
|
* contracts.
|
|
@@ -583,4 +595,15 @@ export const ENDPOINTS = {
|
|
|
583
595
|
path: "/api/maintenance/action",
|
|
584
596
|
description: "Run vacuum, checkpoint, reindex, fts5-rebuild, reconcile-dedup, prune, or integrity-check.",
|
|
585
597
|
} as const satisfies EndpointDef<"POST", MaintenanceAction, MaintenanceActionResult>,
|
|
598
|
+
|
|
599
|
+
// ─── Provider Cache (Sprint A.2) ─────────────────────────────────
|
|
600
|
+
|
|
601
|
+
/** GET /api/provider-cache — Lifetime provider prompt cache aggregates + $ savings. */
|
|
602
|
+
providerCache: {
|
|
603
|
+
method: "GET",
|
|
604
|
+
path: "/api/provider-cache",
|
|
605
|
+
description:
|
|
606
|
+
"Lifetime provider prompt cache hit-rate aggregates + dollar savings estimate.",
|
|
607
|
+
} as const satisfies EndpointDef<"GET", undefined, ProviderCacheResponse>,
|
|
608
|
+
|
|
586
609
|
} as const;
|
|
@@ -144,6 +144,10 @@ export type {
|
|
|
144
144
|
TopicsResponse,
|
|
145
145
|
TopicDetailResponse,
|
|
146
146
|
} from "./endpoints.js";
|
|
147
|
+
export type {
|
|
148
|
+
ProviderCacheSavings,
|
|
149
|
+
ProviderCacheResponse,
|
|
150
|
+
} from "./provider-cache.js";
|
|
147
151
|
|
|
148
152
|
export { ENDPOINTS } from "./endpoints.js";
|
|
149
153
|
|
|
@@ -172,6 +172,12 @@ export interface IndexesIndexRow {
|
|
|
172
172
|
maxTokens: number | null;
|
|
173
173
|
/** Whether reasoning/extended-thinking mode is enabled, or `null` if not configured. */
|
|
174
174
|
reasoning: boolean | null;
|
|
175
|
+
/** Lifetime provider prompt-cache hit percentage, or `null` if no samples. */
|
|
176
|
+
providerCachePct: number | null;
|
|
177
|
+
/** Lifetime provider prompt-cache read tokens, or `null` if no samples. */
|
|
178
|
+
providerCacheRead: number | null;
|
|
179
|
+
/** Lifetime provider prompt-cache write tokens, or `null` if no samples. */
|
|
180
|
+
providerCacheWrite: number | null;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-contracts/provider-cache.ts — Provider prompt cache contract (A.2).
|
|
3
|
+
*
|
|
4
|
+
* Response shape for GET /api/provider-cache: lifetime cache hit-rate
|
|
5
|
+
* aggregates plus optional $ savings estimate priced from model snapshots.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Savings estimate derived from model pricing snapshot. */
|
|
9
|
+
export interface ProviderCacheSavings {
|
|
10
|
+
readonly cacheReadSaved: number;
|
|
11
|
+
readonly cacheWriteCost: number;
|
|
12
|
+
readonly netSaved: number;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly inputRate: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Response body for GET /api/provider-cache. */
|
|
18
|
+
export interface ProviderCacheResponse {
|
|
19
|
+
readonly cache: {
|
|
20
|
+
readonly avgHitPct: number;
|
|
21
|
+
readonly turnCount: number;
|
|
22
|
+
readonly totalCacheRead: number;
|
|
23
|
+
readonly totalCacheWrite: number;
|
|
24
|
+
readonly totalInput: number;
|
|
25
|
+
readonly firstTurnAt: string | null;
|
|
26
|
+
readonly latestTurnAt: string | null;
|
|
27
|
+
};
|
|
28
|
+
readonly savings: ProviderCacheSavings | null;
|
|
29
|
+
readonly updatedAt: string;
|
|
30
|
+
}
|
|
@@ -162,7 +162,7 @@ void _c_achievements;
|
|
|
162
162
|
// (Sprint 39 added /api/sessions + /api/sessions/timeseries.)
|
|
163
163
|
|
|
164
164
|
const ENDPOINT_KEYS = Object.keys(ENDPOINTS) as (keyof typeof ENDPOINTS)[];
|
|
165
|
-
const EXPECTED_ENDPOINT_COUNT =
|
|
165
|
+
const EXPECTED_ENDPOINT_COUNT = 27; // 23 base + 3 S49B maintenance + 1 provider-cache
|
|
166
166
|
|
|
167
167
|
/** All `/api/*` paths served by server.ts (extracted from the route handlers). */
|
|
168
168
|
const SERVER_TS_PATHS: string[] = [
|
|
@@ -181,12 +181,13 @@ const SERVER_TS_PATHS: string[] = [
|
|
|
181
181
|
"/api/sessions",
|
|
182
182
|
"/api/sessions/timeseries",
|
|
183
183
|
"/api/topics",
|
|
184
|
+
"/api/provider-cache",
|
|
184
185
|
];
|
|
185
186
|
|
|
186
187
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
187
188
|
|
|
188
189
|
describe("ENDPOINTS registry", () => {
|
|
189
|
-
test("has exactly
|
|
190
|
+
test("has exactly the expected endpoint count", () => {
|
|
190
191
|
assert.equal(
|
|
191
192
|
ENDPOINT_KEYS.length,
|
|
192
193
|
EXPECTED_ENDPOINT_COUNT,
|
|
@@ -58,6 +58,9 @@ export function readIndex(): IndexIndex | null {
|
|
|
58
58
|
contextWindow: null,
|
|
59
59
|
maxTokens: null,
|
|
60
60
|
reasoning: null,
|
|
61
|
+
providerCachePct: null,
|
|
62
|
+
providerCacheRead: null,
|
|
63
|
+
providerCacheWrite: null,
|
|
61
64
|
}));
|
|
62
65
|
// Enrich each repo with per-store token + model detail read directly via
|
|
63
66
|
// node:sqlite (same zero-dependency invariant as readIndex; no store graph
|
|
@@ -91,6 +94,26 @@ export function readIndex(): IndexIndex | null {
|
|
|
91
94
|
repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
|
|
92
95
|
repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
|
|
93
96
|
}
|
|
97
|
+
// Provider prompt-cache stats (E.3)
|
|
98
|
+
try {
|
|
99
|
+
const pRow = sdb
|
|
100
|
+
.prepare(
|
|
101
|
+
`SELECT
|
|
102
|
+
SUM(cache_read) AS totalRead,
|
|
103
|
+
SUM(cache_write) AS totalWrite,
|
|
104
|
+
SUM(cache_read + cache_write) * 1.0 / MAX(SUM(cache_read + cache_write + tokens_in), 1) AS hitPct
|
|
105
|
+
FROM perf_samples
|
|
106
|
+
WHERE cache_read > 0 OR cache_write > 0`,
|
|
107
|
+
)
|
|
108
|
+
.get() as { totalRead: number; totalWrite: number; hitPct: number } | undefined;
|
|
109
|
+
if (pRow && pRow.totalRead > 0) {
|
|
110
|
+
repo.providerCacheRead = Number(pRow.totalRead);
|
|
111
|
+
repo.providerCacheWrite = Number(pRow.totalWrite);
|
|
112
|
+
repo.providerCachePct = Number((pRow.hitPct * 100).toFixed(1));
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
/* best-effort — perf_samples may not exist */
|
|
116
|
+
}
|
|
94
117
|
} finally {
|
|
95
118
|
sdb.close();
|
|
96
119
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
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 {
|
|
20
|
+
recordPerfSample,
|
|
21
|
+
recordModelSnapshot,
|
|
22
|
+
} from "../../src/store/sqlite.js";
|
|
23
|
+
|
|
24
|
+
const SERVER_ENTRY = new URL("./server.js", import.meta.url).pathname;
|
|
25
|
+
|
|
26
|
+
function waitFor(
|
|
27
|
+
cond: () => boolean | Promise<boolean>,
|
|
28
|
+
timeoutMs = 6000,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const tick = async () => {
|
|
33
|
+
if (await cond()) return resolve();
|
|
34
|
+
if (Date.now() - start > timeoutMs) return reject(new Error("timeout"));
|
|
35
|
+
setTimeout(tick, 50);
|
|
36
|
+
};
|
|
37
|
+
tick();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function freshDir(prefix: string): string {
|
|
42
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function withServer<T>(
|
|
46
|
+
port: string,
|
|
47
|
+
dir: string,
|
|
48
|
+
fn: (port: number) => Promise<T>,
|
|
49
|
+
): Promise<T> {
|
|
50
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = port;
|
|
51
|
+
const child = spawn(process.execPath, [SERVER_ENTRY, dir], {
|
|
52
|
+
stdio: "ignore",
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await waitFor(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
58
|
+
const res = await fetch(`http://localhost:${raw.port}/api/version`);
|
|
59
|
+
return res.ok;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
65
|
+
return await fn(raw.port);
|
|
66
|
+
} finally {
|
|
67
|
+
child.kill("SIGTERM");
|
|
68
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
69
|
+
rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ProviderCacheResponse {
|
|
74
|
+
cache: {
|
|
75
|
+
avgHitPct: number;
|
|
76
|
+
turnCount: number;
|
|
77
|
+
totalCacheRead: number;
|
|
78
|
+
totalCacheWrite: number;
|
|
79
|
+
totalInput: number;
|
|
80
|
+
firstTurnAt: string | null;
|
|
81
|
+
latestTurnAt: string | null;
|
|
82
|
+
};
|
|
83
|
+
savings: {
|
|
84
|
+
cacheReadSaved: number;
|
|
85
|
+
cacheWriteCost: number;
|
|
86
|
+
netSaved: number;
|
|
87
|
+
model: string;
|
|
88
|
+
inputRate: number;
|
|
89
|
+
} | null;
|
|
90
|
+
updatedAt: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("/api/provider-cache", () => {
|
|
94
|
+
test("GET 200 — shape matches ProviderCacheResponse", async () => {
|
|
95
|
+
const dir = freshDir("dash-pcache-agg-");
|
|
96
|
+
// Seed one cache_hit_pct sample with full meta.
|
|
97
|
+
recordPerfSample(dir, "cache_hit_pct", 42, {
|
|
98
|
+
cacheRead: 100,
|
|
99
|
+
cacheWrite: 20,
|
|
100
|
+
input: 500,
|
|
101
|
+
});
|
|
102
|
+
await withServer("19450", dir, async (port) => {
|
|
103
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
104
|
+
assert.equal(res.status, 200);
|
|
105
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
106
|
+
// Shape assertions
|
|
107
|
+
assert.ok("cache" in body, "should have cache");
|
|
108
|
+
assert.ok("savings" in body, "should have savings");
|
|
109
|
+
assert.ok("updatedAt" in body, "should have updatedAt");
|
|
110
|
+
assert.equal(body.cache.turnCount, 1);
|
|
111
|
+
assert.equal(body.cache.totalCacheRead, 100);
|
|
112
|
+
assert.equal(body.cache.totalCacheWrite, 20);
|
|
113
|
+
assert.equal(body.cache.totalInput, 500);
|
|
114
|
+
assert.ok(typeof body.cache.firstTurnAt === "string");
|
|
115
|
+
assert.ok(typeof body.cache.latestTurnAt === "string");
|
|
116
|
+
assert.ok(typeof body.updatedAt === "string");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("GET 200 — empty perf_samples returns zeros/nulls", async () => {
|
|
121
|
+
const dir = freshDir("dash-pcache-empty-");
|
|
122
|
+
await withServer("19451", 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()) as ProviderCacheResponse;
|
|
126
|
+
assert.equal(body.cache.turnCount, 0);
|
|
127
|
+
assert.equal(body.cache.avgHitPct, 0);
|
|
128
|
+
assert.equal(body.cache.totalCacheRead, 0);
|
|
129
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
130
|
+
assert.equal(body.cache.totalInput, 0);
|
|
131
|
+
assert.equal(body.cache.firstTurnAt, null);
|
|
132
|
+
assert.equal(body.cache.latestTurnAt, null);
|
|
133
|
+
assert.equal(body.savings, null);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("POST → 405", async () => {
|
|
138
|
+
const dir = freshDir("dash-pcache-meth-");
|
|
139
|
+
await withServer("19452", dir, async (port) => {
|
|
140
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
});
|
|
143
|
+
assert.equal(res.status, 405);
|
|
144
|
+
const body = (await res.json()) as { error: string };
|
|
145
|
+
assert.equal(body.error, "method_not_allowed");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("no model snapshot → savings: null", async () => {
|
|
150
|
+
const dir = freshDir("dash-pcache-nosnap-");
|
|
151
|
+
recordPerfSample(dir, "cache_hit_pct", 50, {
|
|
152
|
+
cacheRead: 200,
|
|
153
|
+
cacheWrite: 30,
|
|
154
|
+
input: 1000,
|
|
155
|
+
});
|
|
156
|
+
await withServer("19453", dir, async (port) => {
|
|
157
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
158
|
+
assert.equal(res.status, 200);
|
|
159
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
160
|
+
assert.equal(body.savings, null);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("with model snapshot → priced savings fields", async () => {
|
|
165
|
+
const dir = freshDir("dash-pcache-savings-");
|
|
166
|
+
recordPerfSample(dir, "cache_hit_pct", 50, {
|
|
167
|
+
cacheRead: 2000,
|
|
168
|
+
cacheWrite: 400,
|
|
169
|
+
input: 10000,
|
|
170
|
+
});
|
|
171
|
+
// Record a model snapshot with a known input rate.
|
|
172
|
+
recordModelSnapshot(
|
|
173
|
+
"/tmp/test-repo",
|
|
174
|
+
{
|
|
175
|
+
provider: "anthropic",
|
|
176
|
+
providerName: "Anthropic",
|
|
177
|
+
modelId: "claude-sonnet-4-20250514",
|
|
178
|
+
modelName: "Claude Sonnet 4",
|
|
179
|
+
inputRate: 3e-6, // $3 / 1M tokens
|
|
180
|
+
outputRate: 15e-6,
|
|
181
|
+
contextWindow: 200000,
|
|
182
|
+
maxTokens: 32000,
|
|
183
|
+
reasoning: false,
|
|
184
|
+
},
|
|
185
|
+
dir,
|
|
186
|
+
);
|
|
187
|
+
await withServer("19454", dir, async (port) => {
|
|
188
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
189
|
+
assert.equal(res.status, 200);
|
|
190
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
191
|
+
assert.ok(body.savings, "savings should not be null");
|
|
192
|
+
// cacheReadSaved = 2000 * 3e-6 * 0.9 = 0.0054
|
|
193
|
+
assert.ok(body.savings!.cacheReadSaved > 0, "cacheReadSaved > 0");
|
|
194
|
+
// cacheWriteCost = 400 * 3e-6 * 0.25 = 0.0003
|
|
195
|
+
assert.ok(body.savings!.cacheWriteCost > 0, "cacheWriteCost > 0");
|
|
196
|
+
assert.ok(
|
|
197
|
+
body.savings!.netSaved ===
|
|
198
|
+
body.savings!.cacheReadSaved - body.savings!.cacheWriteCost,
|
|
199
|
+
"netSaved = cacheReadSaved - cacheWriteCost",
|
|
200
|
+
);
|
|
201
|
+
assert.equal(body.savings!.model, "Claude Sonnet 4");
|
|
202
|
+
assert.equal(body.savings!.inputRate, 3e-6);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("partial meta (missing fields) → treated as 0", async () => {
|
|
207
|
+
const dir = freshDir("dash-pcache-partial-");
|
|
208
|
+
// Only input in meta; cacheRead/cacheWrite missing → treated as 0.
|
|
209
|
+
recordPerfSample(dir, "cache_hit_pct", 30, { input: 800 });
|
|
210
|
+
// Only cacheRead in meta; input/cacheWrite missing → treated as 0.
|
|
211
|
+
recordPerfSample(dir, "cache_hit_pct", 60, { cacheRead: 150 });
|
|
212
|
+
await withServer("19455", dir, async (port) => {
|
|
213
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
214
|
+
assert.equal(res.status, 200);
|
|
215
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
216
|
+
// First sample: input=800, cacheRead=0, cacheWrite=0
|
|
217
|
+
// Second sample: cacheRead=150, input=0, cacheWrite=0
|
|
218
|
+
assert.equal(body.cache.turnCount, 2);
|
|
219
|
+
assert.equal(body.cache.totalCacheRead, 150);
|
|
220
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
221
|
+
assert.equal(body.cache.totalInput, 800);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("NULL meta → counted in turnCount, contributes 0 tokens + 0 avg", async () => {
|
|
226
|
+
const dir = freshDir("dash-pcache-nullmeta-");
|
|
227
|
+
// avgHitPct is derived from (cacheRead / (cacheRead + input + cacheWrite)) * 100,
|
|
228
|
+
// NOT from the `value` column. NULL meta → no tokens → avg 0, not 25.
|
|
229
|
+
recordPerfSample(dir, "cache_hit_pct", 25, undefined);
|
|
230
|
+
await withServer("19456", dir, async (port) => {
|
|
231
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
232
|
+
assert.equal(res.status, 200);
|
|
233
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
234
|
+
assert.equal(body.cache.turnCount, 1);
|
|
235
|
+
assert.equal(body.cache.avgHitPct, 0);
|
|
236
|
+
assert.equal(body.cache.totalCacheRead, 0);
|
|
237
|
+
assert.equal(body.cache.totalCacheWrite, 0);
|
|
238
|
+
assert.equal(body.cache.totalInput, 0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("model snapshot with modelName=null → falls back to modelId", async () => {
|
|
243
|
+
const dir = freshDir("dash-pcache-modelid-");
|
|
244
|
+
recordPerfSample(dir, "cache_hit_pct", 10, {
|
|
245
|
+
cacheRead: 100,
|
|
246
|
+
cacheWrite: 10,
|
|
247
|
+
input: 1000,
|
|
248
|
+
});
|
|
249
|
+
recordModelSnapshot(
|
|
250
|
+
"/tmp/test-repo",
|
|
251
|
+
{
|
|
252
|
+
provider: "openai",
|
|
253
|
+
providerName: null,
|
|
254
|
+
modelId: "gpt-4o",
|
|
255
|
+
modelName: null,
|
|
256
|
+
inputRate: 2.5e-6,
|
|
257
|
+
outputRate: 10e-6,
|
|
258
|
+
contextWindow: 128000,
|
|
259
|
+
maxTokens: 16384,
|
|
260
|
+
reasoning: false,
|
|
261
|
+
},
|
|
262
|
+
dir,
|
|
263
|
+
);
|
|
264
|
+
await withServer("19457", dir, async (port) => {
|
|
265
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
266
|
+
assert.equal(res.status, 200);
|
|
267
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
268
|
+
assert.ok(body.savings);
|
|
269
|
+
assert.equal(body.savings!.model, "gpt-4o");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("model snapshot with zero inputRate → savings: null", async () => {
|
|
274
|
+
const dir = freshDir("dash-pcache-zerorate-");
|
|
275
|
+
recordPerfSample(dir, "cache_hit_pct", 10, {
|
|
276
|
+
cacheRead: 100,
|
|
277
|
+
cacheWrite: 10,
|
|
278
|
+
input: 1000,
|
|
279
|
+
});
|
|
280
|
+
recordModelSnapshot(
|
|
281
|
+
"/tmp/test-repo",
|
|
282
|
+
{
|
|
283
|
+
provider: "local",
|
|
284
|
+
providerName: null,
|
|
285
|
+
modelId: "local-llm",
|
|
286
|
+
modelName: null,
|
|
287
|
+
inputRate: 0,
|
|
288
|
+
outputRate: 0,
|
|
289
|
+
contextWindow: 4096,
|
|
290
|
+
maxTokens: 2048,
|
|
291
|
+
reasoning: false,
|
|
292
|
+
},
|
|
293
|
+
dir,
|
|
294
|
+
);
|
|
295
|
+
await withServer("19458", dir, async (port) => {
|
|
296
|
+
const res = await fetch(`http://localhost:${port}/api/provider-cache`);
|
|
297
|
+
assert.equal(res.status, 200);
|
|
298
|
+
const body = (await res.json()) as ProviderCacheResponse;
|
|
299
|
+
assert.equal(body.savings, null);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
13
|
+
|
|
14
|
+
import type { RouteContext } from "./routes-core.js";
|
|
15
|
+
import { computeCacheSavings } from "../../src/pricing.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// handleProviderCache — "/api/provider-cache"
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function handleProviderCache(
|
|
22
|
+
req: IncomingMessage,
|
|
23
|
+
res: ServerResponse,
|
|
24
|
+
ctx: RouteContext,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (!req.url?.startsWith("/api/provider-cache")) return false;
|
|
27
|
+
|
|
28
|
+
const { stateDir } = ctx;
|
|
29
|
+
|
|
30
|
+
// /api/provider-cache — GET returns lifetime provider prompt cache aggregates
|
|
31
|
+
// from perf_samples (cache_hit_pct rows with meta JSON) plus an optional
|
|
32
|
+
// dollar savings estimate priced from the latest model snapshot. The dashboard
|
|
33
|
+
// server is a detached child with no MegaRuntime ref, so it reads via a
|
|
34
|
+
// require()'d sqlite helper (same pattern as /api/perf). Non-GET → 405.
|
|
35
|
+
// PREVENT-PI-004: loopback.
|
|
36
|
+
const pfReq = createRequire(import.meta.url);
|
|
37
|
+
const { readProviderCacheLifetime, latestModelSnapshot } = pfReq(
|
|
38
|
+
"../../src/store/sqlite.js",
|
|
39
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
40
|
+
if (req.method !== "GET") {
|
|
41
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
42
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
43
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const lifetime = readProviderCacheLifetime(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
48
|
+
|
|
49
|
+
// D3: compute dollar savings from the latest model snapshot. Null-safe:
|
|
50
|
+
// when no snapshot or zero input rate, savings stays null (UI shows "—").
|
|
51
|
+
let savings: {
|
|
52
|
+
cacheReadSaved: number;
|
|
53
|
+
cacheWriteCost: number;
|
|
54
|
+
netSaved: number;
|
|
55
|
+
model: string;
|
|
56
|
+
inputRate: number;
|
|
57
|
+
} | null = null;
|
|
58
|
+
try {
|
|
59
|
+
const snap = latestModelSnapshot(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
60
|
+
if (snap && snap.inputRate > 0) {
|
|
61
|
+
const s = computeCacheSavings(
|
|
62
|
+
lifetime.totalCacheRead,
|
|
63
|
+
lifetime.totalCacheWrite,
|
|
64
|
+
snap.inputRate,
|
|
65
|
+
);
|
|
66
|
+
savings = {
|
|
67
|
+
cacheReadSaved: s.cacheReadSaved,
|
|
68
|
+
cacheWriteCost: s.cacheWriteCost,
|
|
69
|
+
netSaved: s.netSaved,
|
|
70
|
+
model: snap.modelName ?? snap.modelId ?? "unknown",
|
|
71
|
+
inputRate: snap.inputRate,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* pricing unavailable — savings stays null */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body: {
|
|
79
|
+
cache: {
|
|
80
|
+
avgHitPct: number;
|
|
81
|
+
turnCount: number;
|
|
82
|
+
totalCacheRead: number;
|
|
83
|
+
totalCacheWrite: number;
|
|
84
|
+
totalInput: number;
|
|
85
|
+
firstTurnAt: string | null;
|
|
86
|
+
latestTurnAt: string | null;
|
|
87
|
+
};
|
|
88
|
+
savings: {
|
|
89
|
+
cacheReadSaved: number;
|
|
90
|
+
cacheWriteCost: number;
|
|
91
|
+
netSaved: number;
|
|
92
|
+
model: string;
|
|
93
|
+
inputRate: number;
|
|
94
|
+
} | null;
|
|
95
|
+
updatedAt: string;
|
|
96
|
+
} = {
|
|
97
|
+
cache: {
|
|
98
|
+
avgHitPct: lifetime.avgHitPct,
|
|
99
|
+
turnCount: lifetime.sampleCount,
|
|
100
|
+
totalCacheRead: lifetime.totalCacheRead,
|
|
101
|
+
totalCacheWrite: lifetime.totalCacheWrite,
|
|
102
|
+
totalInput: lifetime.totalInput,
|
|
103
|
+
firstTurnAt: lifetime.firstSampleAt,
|
|
104
|
+
latestTurnAt: lifetime.latestSampleAt,
|
|
105
|
+
},
|
|
106
|
+
savings,
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
110
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
111
|
+
res.end(JSON.stringify(body));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
114
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
115
|
+
res.end(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
error: "provider_cache_unavailable",
|
|
118
|
+
detail: String(e),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
@@ -140,9 +140,9 @@ function sendSchemaHealth(res: ServerResponse, ctx: RouteContext): void {
|
|
|
140
140
|
const schemaVersion = metaRow ? Number(metaRow.value) : 0;
|
|
141
141
|
|
|
142
142
|
// PRAGMA foreign_key_check
|
|
143
|
-
const fkRows = db
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const fkRows = db.prepare("PRAGMA foreign_key_check").all() as Array<
|
|
144
|
+
Record<string, unknown>
|
|
145
|
+
>;
|
|
146
146
|
const fkCheck = fkRows.map((r) => JSON.stringify(r));
|
|
147
147
|
|
|
148
148
|
// Per-column audit: every ensureColumn migration column declared in
|
|
@@ -152,7 +152,11 @@ function sendSchemaHealth(res: ServerResponse, ctx: RouteContext): void {
|
|
|
152
152
|
column: string;
|
|
153
153
|
decl: string;
|
|
154
154
|
}> = [
|
|
155
|
-
{
|
|
155
|
+
{
|
|
156
|
+
table: "context_chunks",
|
|
157
|
+
column: "original_token_estimate",
|
|
158
|
+
decl: "INTEGER",
|
|
159
|
+
},
|
|
156
160
|
{ table: "raw_transcript", column: "content_ref", decl: "TEXT" },
|
|
157
161
|
{ table: "memories", column: "category", decl: "TEXT" },
|
|
158
162
|
{ table: "memories", column: "target", decl: "TEXT" },
|
|
@@ -165,9 +169,9 @@ function sendSchemaHealth(res: ServerResponse, ctx: RouteContext): void {
|
|
|
165
169
|
];
|
|
166
170
|
|
|
167
171
|
const columns = expectedColumns.map((ec) => {
|
|
168
|
-
const cols = db
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
const cols = db.prepare(`PRAGMA table_info(${ec.table})`).all() as Array<{
|
|
173
|
+
name: string;
|
|
174
|
+
}>;
|
|
171
175
|
const present = cols.some((c) => c.name === ec.column);
|
|
172
176
|
return {
|
|
173
177
|
table: ec.table,
|
|
@@ -223,7 +227,7 @@ function handleMaintenanceAction(
|
|
|
223
227
|
return;
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
try {
|
|
227
231
|
const m = lazyMaintenance();
|
|
228
232
|
const u = lazyStoreUtils();
|
|
229
233
|
// All destructive actions: temp backup first
|
|
@@ -284,9 +288,12 @@ function handleMaintenanceAction(
|
|
|
284
288
|
case "reconcile-dedup": {
|
|
285
289
|
const r = m.reconcileDedupMirror(ctx.stateDir);
|
|
286
290
|
const lines: string[] = [];
|
|
287
|
-
if (r.fixedRefCount)
|
|
288
|
-
|
|
289
|
-
if (r.
|
|
291
|
+
if (r.fixedRefCount)
|
|
292
|
+
lines.push(`${r.fixedRefCount} ref_counts corrected`);
|
|
293
|
+
if (r.orphansDeleted)
|
|
294
|
+
lines.push(`${r.orphansDeleted} orphans deleted`);
|
|
295
|
+
if (r.refsBackfilled)
|
|
296
|
+
lines.push(`${r.refsBackfilled} refs backfilled`);
|
|
290
297
|
sendJson(res, 200, {
|
|
291
298
|
operation: "reconcile-dedup",
|
|
292
299
|
success: true,
|