pi-mega-compact 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +19 -0
  2. package/dist/extensions/dashboard-server/api-contracts/maintenance.js +7 -0
  3. package/dist/extensions/dashboard-server/api-contracts.test.js +1 -1
  4. package/dist/extensions/dashboard-server/routes-maintenance.js +293 -0
  5. package/dist/extensions/dashboard-server/routes.js +1 -0
  6. package/dist/extensions/dashboard-server/server.js +3 -1
  7. package/dist/src/store/sqlite/maintenance.js +20 -1
  8. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DZp_xQOW.js → AchievementTiles-Cml6YC8w.js} +2 -2
  9. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DZp_xQOW.js.map → AchievementTiles-Cml6YC8w.js.map} +1 -1
  10. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BzFHpAHM.js → AchievementsTab-B8NqDt7I.js} +2 -2
  11. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BzFHpAHM.js.map → AchievementsTab-B8NqDt7I.js.map} +1 -1
  12. package/extensions/dashboard-client/dist/assets/{CacheTab-DEQbTq2T.js → CacheTab-BCLzVL4S.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{CacheTab-DEQbTq2T.js.map → CacheTab-BCLzVL4S.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{ConfigTab-DQ4FG33J.js → ConfigTab-CBYvp2mV.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{ConfigTab-DQ4FG33J.js.map → ConfigTab-CBYvp2mV.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js → EventsTab-DlTJqiDm.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js.map → EventsTab-DlTJqiDm.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.js → GameTab-DkGTqKJW.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.js.map → GameTab-DkGTqKJW.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +2 -0
  21. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +1 -0
  22. package/extensions/dashboard-client/dist/assets/{MetricsTab-D_XnsEYX.js → MetricsTab-BFo7pKM-.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{MetricsTab-D_XnsEYX.js.map → MetricsTab-BFo7pKM-.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.js → OverviewTab-CClCUd3-.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.js.map → OverviewTab-CClCUd3-.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{ReposTab-DuyhOktG.js → ReposTab-_3R4RSLZ.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{ReposTab-DuyhOktG.js.map → ReposTab-_3R4RSLZ.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{SessionsTab-mz_vz8i2.js → SessionsTab-CCYd-kSz.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{SessionsTab-mz_vz8i2.js.map → SessionsTab-CCYd-kSz.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B8AIjNTY.js → TimeSavedCard-0TqsrY6y.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B8AIjNTY.js.map → TimeSavedCard-0TqsrY6y.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{TopicsTab-DKuct4oD.js → TopicsTab-DRLJmVv0.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{TopicsTab-DKuct4oD.js.map → TopicsTab-DRLJmVv0.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.js → TurnsTab-CLNM0T5R.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.js.map → TurnsTab-CLNM0T5R.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{index-CqLN7STl.js → index-BcoSz-L4.js} +10 -10
  37. package/extensions/dashboard-client/dist/assets/{index-CqLN7STl.js.map → index-BcoSz-L4.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js → useSSE-Cezlih5V.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js.map → useSSE-Cezlih5V.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/index.html +1 -1
  41. package/extensions/dashboard-client/src/App.tsx +5 -1
  42. package/extensions/dashboard-client/src/api/client.ts +23 -0
  43. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +293 -0
  44. package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -0
  45. package/extensions/dashboard-server/api-contracts/index.ts +12 -0
  46. package/extensions/dashboard-server/api-contracts/maintenance.ts +84 -0
  47. package/extensions/dashboard-server/api-contracts.test.ts +1 -1
  48. package/extensions/dashboard-server/api-contracts.ts +9 -0
  49. package/extensions/dashboard-server/routes-maintenance.ts +345 -0
  50. package/extensions/dashboard-server/routes.ts +1 -0
  51. package/extensions/dashboard-server/server.ts +2 -0
  52. package/package.json +1 -1
  53. package/src/store/sqlite/maintenance.ts +20 -1
@@ -0,0 +1,345 @@
1
+ /**
2
+ * dashboard-server/routes-maintenance.ts — S49B Maintenance tab API routes.
3
+ *
4
+ * Handlers for:
5
+ * GET /api/maintenance — DB stats (table row counts + storage)
6
+ * GET /api/maintenance/schema-health — SCHEMA_VERSION + integrity audit
7
+ * POST /api/maintenance/action — Trigger a maintenance action
8
+ *
9
+ * All pi-agnostic; lazy-requires maintenance primitives to avoid a top-level
10
+ * import of the store from the dashboard server process. PREVENT-PI-004: local
11
+ * SQLite only; zero network calls.
12
+ */
13
+
14
+ import { createRequire } from "node:module";
15
+ import type { IncomingMessage, ServerResponse } from "node:http";
16
+
17
+ import type { RouteContext } from "./routes-core.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Lazy imports (via createRequire)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const lazyMaintenance = (() => {
24
+ let mod: typeof import("../../src/store/sqlite/maintenance.js") | null = null;
25
+ return () => {
26
+ if (!mod)
27
+ mod = createRequire(import.meta.url)(
28
+ "../../src/store/sqlite/maintenance.js",
29
+ ) as typeof import("../../src/store/sqlite/maintenance.js");
30
+ return mod;
31
+ };
32
+ })();
33
+
34
+ const lazyStoreUtils = (() => {
35
+ let mod: typeof import("../../src/store/sqlite/utils.js") | null = null;
36
+ return () => {
37
+ if (!mod)
38
+ mod = createRequire(import.meta.url)(
39
+ "../../src/store/sqlite/utils.js",
40
+ ) as typeof import("../../src/store/sqlite/utils.js");
41
+ return mod;
42
+ };
43
+ })();
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Types for JSON deserialisation
47
+ // ---------------------------------------------------------------------------
48
+
49
+ interface MaintenanceActionBody {
50
+ action: string;
51
+ daysOld?: number | string;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // GET /api/maintenance and sub-paths
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function handleMaintenance(
59
+ req: IncomingMessage,
60
+ res: ServerResponse,
61
+ ctx: RouteContext,
62
+ ): boolean {
63
+ const url = req.url ?? "";
64
+ if (!url.startsWith("/api/maintenance")) return false;
65
+
66
+ const method = (req.method ?? "GET").toUpperCase();
67
+
68
+ // ── GET /api/maintenance/schema-health ────────────────────────────
69
+ if (method === "GET" && url === "/api/maintenance/schema-health") {
70
+ sendSchemaHealth(res, ctx);
71
+ return true;
72
+ }
73
+
74
+ // ── POST /api/maintenance/action ──────────────────────────────────
75
+ if (method === "POST" && url === "/api/maintenance/action") {
76
+ handleMaintenanceAction(req, res, ctx);
77
+ return true;
78
+ }
79
+
80
+ // ── GET /api/maintenance ──────────────────────────────────────────
81
+ if (method === "GET" && url === "/api/maintenance") {
82
+ sendDbStats(res, ctx);
83
+ return true;
84
+ }
85
+
86
+ // ── Unknown sub-path ──────────────────────────────────────────────
87
+ sendJson(res, 404, { error: "not found" });
88
+ return true;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // GET /api/maintenance — DB stats
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function sendDbStats(res: ServerResponse, ctx: RouteContext): void {
96
+ try {
97
+ const { getDbStats } = lazyMaintenance();
98
+ const raw = getDbStats(ctx.stateDir);
99
+
100
+ const tables = Object.entries(raw.tableCounts).map(([table, rowCount]) => ({
101
+ table,
102
+ rowCount,
103
+ }));
104
+
105
+ sendJson(res, 200, {
106
+ type: "db-stats" as const,
107
+ tables,
108
+ storage: {
109
+ files: {
110
+ dbBytes: raw.dbBytes,
111
+ walBytes: raw.walBytes,
112
+ shmBytes: raw.shmBytes,
113
+ },
114
+ pageSize: raw.pageSize,
115
+ pageCount: raw.pageCount,
116
+ freelistPages: raw.freelistPages,
117
+ },
118
+ });
119
+ } catch (err) {
120
+ sendJson(res, 500, { error: (err as Error).message });
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // GET /api/maintenance/schema-health — schema version + integrity audit
126
+ // ---------------------------------------------------------------------------
127
+
128
+ function sendSchemaHealth(res: ServerResponse, ctx: RouteContext): void {
129
+ try {
130
+ const { integrityCheck } = lazyMaintenance();
131
+ const integrity = integrityCheck(ctx.stateDir);
132
+
133
+ const { openStore } = lazyStoreUtils();
134
+ const db = openStore(ctx.stateDir);
135
+
136
+ // SCHEMA_VERSION from meta table
137
+ const metaRow = db
138
+ .prepare("SELECT value FROM meta WHERE key = ?")
139
+ .get("schema_version") as { value: string } | undefined;
140
+ const schemaVersion = metaRow ? Number(metaRow.value) : 0;
141
+
142
+ // PRAGMA foreign_key_check
143
+ const fkRows = db
144
+ .prepare("PRAGMA foreign_key_check")
145
+ .all() as Array<Record<string, unknown>>;
146
+ const fkCheck = fkRows.map((r) => JSON.stringify(r));
147
+
148
+ // Per-column audit: every ensureColumn migration column declared in
149
+ // schema.ts, checked via PRAGMA table_info (read-only).
150
+ const expectedColumns: Array<{
151
+ table: string;
152
+ column: string;
153
+ decl: string;
154
+ }> = [
155
+ { table: "context_chunks", column: "original_token_estimate", decl: "INTEGER" },
156
+ { table: "raw_transcript", column: "content_ref", decl: "TEXT" },
157
+ { table: "memories", column: "category", decl: "TEXT" },
158
+ { table: "memories", column: "target", decl: "TEXT" },
159
+ { table: "memories", column: "last_referenced", decl: "INTEGER" },
160
+ { table: "memories", column: "source_turn", decl: "INTEGER" },
161
+ { table: "raptor_nodes", column: "built_at", decl: "INTEGER" },
162
+ { table: "raw_transcript", column: "turn_index", decl: "INTEGER" },
163
+ { table: "session_state", column: "conversation_id", decl: "TEXT" },
164
+ { table: "session_state", column: "last_turn_id", decl: "INTEGER" },
165
+ ];
166
+
167
+ const columns = expectedColumns.map((ec) => {
168
+ const cols = db
169
+ .prepare(`PRAGMA table_info(${ec.table})`)
170
+ .all() as Array<{ name: string }>;
171
+ const present = cols.some((c) => c.name === ec.column);
172
+ return {
173
+ table: ec.table,
174
+ column: ec.column,
175
+ present,
176
+ expectedDecl: ec.decl,
177
+ };
178
+ });
179
+
180
+ const healthy =
181
+ integrity.length === 1 &&
182
+ integrity[0] === "ok" &&
183
+ columns.every((c) => c.present);
184
+
185
+ sendJson(res, 200, {
186
+ type: "schema-health" as const,
187
+ schemaVersion,
188
+ integrity,
189
+ fkCheck,
190
+ columns,
191
+ healthy,
192
+ });
193
+ } catch (err) {
194
+ sendJson(res, 500, { error: (err as Error).message });
195
+ }
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // POST /api/maintenance/action — Trigger a maintenance action
200
+ // ---------------------------------------------------------------------------
201
+
202
+ function handleMaintenanceAction(
203
+ req: IncomingMessage,
204
+ res: ServerResponse,
205
+ ctx: RouteContext,
206
+ ): void {
207
+ let body = "";
208
+ req.setEncoding("utf8");
209
+ req.on("data", (chunk: string) => {
210
+ body += chunk;
211
+ if (body.length > 10_000) req.destroy();
212
+ });
213
+ req.on("end", () => {
214
+ let payload: MaintenanceActionBody;
215
+ try {
216
+ payload = JSON.parse(body) as MaintenanceActionBody;
217
+ if (!payload || !payload.action) {
218
+ sendJson(res, 400, { error: "missing 'action' field" });
219
+ return;
220
+ }
221
+ } catch {
222
+ sendJson(res, 400, { error: "invalid JSON" });
223
+ return;
224
+ }
225
+
226
+ try {
227
+ const m = lazyMaintenance();
228
+ const u = lazyStoreUtils();
229
+ // All destructive actions: temp backup first
230
+ if (payload.action !== "integrity-check") {
231
+ // Non-blocking — backup failure is non-fatal
232
+ m.backupDb(ctx.stateDir);
233
+ }
234
+
235
+ switch (payload.action) {
236
+ case "vacuum": {
237
+ const r = m.vacuumDb(ctx.stateDir);
238
+ sendJson(res, 200, {
239
+ operation: "vacuum",
240
+ success: true,
241
+ affected: r.affected,
242
+ reclaimedBytes: r.reclaimedBytes,
243
+ summary: r.summary,
244
+ });
245
+ return;
246
+ }
247
+ case "checkpoint": {
248
+ const r = m.checkpointWal(ctx.stateDir);
249
+ sendJson(res, 200, {
250
+ operation: "checkpoint",
251
+ success: true,
252
+ affected: r.affected,
253
+ reclaimedBytes: r.reclaimedBytes,
254
+ summary: r.summary,
255
+ });
256
+ return;
257
+ }
258
+ case "reindex": {
259
+ const db = u.openStore(ctx.stateDir);
260
+ db.exec("REINDEX");
261
+ sendJson(res, 200, {
262
+ operation: "reindex",
263
+ success: true,
264
+ affected: 0,
265
+ reclaimedBytes: 0,
266
+ summary: "REINDEX completed",
267
+ });
268
+ return;
269
+ }
270
+ case "fts5-rebuild": {
271
+ const db = u.openStore(ctx.stateDir);
272
+ db.exec(
273
+ "INSERT INTO context_chunks_trgm(context_chunks_trgm) VALUES('rebuild')",
274
+ );
275
+ sendJson(res, 200, {
276
+ operation: "fts5-rebuild",
277
+ success: true,
278
+ affected: 0,
279
+ reclaimedBytes: 0,
280
+ summary: "FTS5 rebuild triggered",
281
+ });
282
+ return;
283
+ }
284
+ case "reconcile-dedup": {
285
+ const r = m.reconcileDedupMirror(ctx.stateDir);
286
+ const lines: string[] = [];
287
+ if (r.fixedRefCount) lines.push(`${r.fixedRefCount} ref_counts corrected`);
288
+ if (r.orphansDeleted) lines.push(`${r.orphansDeleted} orphans deleted`);
289
+ if (r.refsBackfilled) lines.push(`${r.refsBackfilled} refs backfilled`);
290
+ sendJson(res, 200, {
291
+ operation: "reconcile-dedup",
292
+ success: true,
293
+ affected: r.fixedRefCount + r.orphansDeleted + r.refsBackfilled,
294
+ reclaimedBytes: 0,
295
+ summary: lines.length ? lines.join(", ") : "nothing to fix",
296
+ });
297
+ return;
298
+ }
299
+ case "prune": {
300
+ const days = Number(payload.daysOld) || 30;
301
+ const r = m.pruneOldRows(ctx.stateDir, days);
302
+ sendJson(res, 200, {
303
+ operation: "prune",
304
+ success: true,
305
+ affected: r.affected,
306
+ reclaimedBytes: r.reclaimedBytes,
307
+ summary: r.summary,
308
+ });
309
+ return;
310
+ }
311
+ case "integrity-check": {
312
+ const lines = m.integrityCheck(ctx.stateDir);
313
+ sendJson(res, 200, {
314
+ operation: "integrity-check",
315
+ success: lines.length === 1 && lines[0] === "ok",
316
+ affected: 0,
317
+ reclaimedBytes: 0,
318
+ summary: lines.join("; "),
319
+ detail: lines,
320
+ });
321
+ return;
322
+ }
323
+ default:
324
+ sendJson(res, 400, {
325
+ error: `unknown action: ${payload.action}`,
326
+ });
327
+ }
328
+ } catch (err) {
329
+ sendJson(res, 500, { error: (err as Error).message });
330
+ }
331
+ });
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // JSON helper
336
+ // ---------------------------------------------------------------------------
337
+
338
+ function sendJson(res: ServerResponse, status: number, data: unknown): void {
339
+ const body = JSON.stringify(data);
340
+ res.writeHead(status, {
341
+ "Content-Type": "application/json",
342
+ "Content-Length": Buffer.byteLength(body),
343
+ });
344
+ res.end(body);
345
+ }
@@ -18,3 +18,4 @@ export {
18
18
  export { handleEvents, handleSessions } from "./routes-sessions.js";
19
19
  export { handleTopics } from "./routes-topics.js";
20
20
  export { handleTurns } from "./routes-turns.js";
21
+ export { handleMaintenance } from "./routes-maintenance.js";
@@ -36,6 +36,7 @@ import {
36
36
  handleSessions,
37
37
  handleTopics,
38
38
  handleTurns,
39
+ handleMaintenance,
39
40
  handleStatic,
40
41
  } from "./routes.js";
41
42
 
@@ -223,6 +224,7 @@ export async function launchDashboardServer(
223
224
  if (handleSessions(req, res, ctx)) return;
224
225
  if (handleTopics(req, res, ctx)) return;
225
226
  if (handleTurns(req, res, ctx)) return;
227
+ if (handleMaintenance(req, res, ctx)) return;
226
228
  handleStatic(req, res, ctx);
227
229
  });
228
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -4,8 +4,9 @@
4
4
  * All pi-agnostic, all parameterized (PREVENT-002), all local (PREVENT-PI-004).
5
5
  * Exposed via the /mega-db-* slash commands in extensions/mega-db-cmds.ts.
6
6
  */
7
- import { statSync } from "node:fs";
7
+ import { statSync, mkdirSync, existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
9
10
  import { getStateDir } from "../../store.js";
10
11
  import { openStore, withTx } from "./utils.js";
11
12
 
@@ -100,6 +101,24 @@ export interface MaintenanceResult {
100
101
  summary: string;
101
102
  }
102
103
 
104
+ /**
105
+ * Lightweight temp backup of the main sqlite.db via VACUUM INTO.
106
+ *
107
+ * Creates a one-off backup in the OS tmpdir. Returns the backup path.
108
+ * VACUUM INTO produces a clean, defragmented copy — also serves as a
109
+ * pre-op safeguard for any destructive maintenance action.
110
+ */
111
+ export function backupDb(stateDir: string = getStateDir()): string {
112
+ const backupDir = join(tmpdir(), "pi-mega-compact-backups");
113
+ if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
114
+ const ts = Date.now();
115
+ const backupPath = join(backupDir, `sqlite-${ts}.db`);
116
+ const db = openStore(stateDir);
117
+ // guardrails-disable-next-line PREVENT-002: path built from tmpdir + timestamp, not user input
118
+ db.exec(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`);
119
+ return backupPath;
120
+ }
121
+
103
122
  /**
104
123
  * Prune raw_transcript + checkpoint_epochs rows older than `daysOld`.
105
124
  * Uses `message_timestamp` (raw_transcript) and `created_at` (epochs), both