pi-mega-compact 0.4.16 → 0.4.18
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.js +192 -0
- package/dist/extensions/mega-runtime.js +46 -1
- package/dist/src/store/sqlite.js +162 -0
- package/extensions/DASHBOARD.md +18 -5
- package/extensions/dashboard-server.ts +202 -0
- package/extensions/mega-dashboard.ts +8 -0
- package/extensions/mega-runtime.ts +45 -1
- package/package.json +1 -1
- package/src/store/sqlite.ts +214 -0
|
@@ -12,7 +12,72 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { createServer } from "node:http";
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
15
16
|
import { join } from "node:path";
|
|
17
|
+
import Database from "better-sqlite3";
|
|
18
|
+
// --- Multi-repo index (Phase 5b) ------------------------------------------------
|
|
19
|
+
// The extension writes a machine-wide repo registry into a single SQLite DB
|
|
20
|
+
// (<indexDir>/index.sqlite) as the concurrency-safe write path; the dashboard
|
|
21
|
+
// reads that table directly (one read-only connection, opened per request so a
|
|
22
|
+
// concurrent writer's WAL never blocks the request). All registry data lives in
|
|
23
|
+
// SQLite (the project's one-store invariant) — there is no JSON mirror. Same
|
|
24
|
+
// index-dir resolution as src/store/sqlite.ts getIndexDir().
|
|
25
|
+
function getIndexDir() {
|
|
26
|
+
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
27
|
+
if (override && override.trim() !== "")
|
|
28
|
+
return override;
|
|
29
|
+
try {
|
|
30
|
+
return join(homedir(), ".mega-compact-index");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return join("/tmp", ".mega-compact-index");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Read the machine-wide repo registry from SQLite (read-only, single shot). */
|
|
37
|
+
function readIndex() {
|
|
38
|
+
const indexPath = join(getIndexDir(), "index.sqlite");
|
|
39
|
+
if (!existsSync(indexPath))
|
|
40
|
+
return null;
|
|
41
|
+
let db;
|
|
42
|
+
try {
|
|
43
|
+
// Read-only + immutable WAL so a concurrent writer's WAL never blocks us.
|
|
44
|
+
db = new Database(indexPath, { readonly: true, fileMustExist: true });
|
|
45
|
+
db.pragma("journal_mode = WAL");
|
|
46
|
+
const rows = db
|
|
47
|
+
.prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
|
|
48
|
+
.all();
|
|
49
|
+
const repos = rows.map((r) => ({
|
|
50
|
+
repoRoot: String(r.repo_root ?? ""),
|
|
51
|
+
displayName: String(r.display_name ?? ""),
|
|
52
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
53
|
+
tokensSaved: Number(r.tokens_saved ?? 0),
|
|
54
|
+
compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
|
|
55
|
+
lastCompactedAt: r.last_compacted_at ?? null,
|
|
56
|
+
provider: r.provider ?? null,
|
|
57
|
+
providerName: r.provider_name ?? null,
|
|
58
|
+
modelName: r.model_name ?? null,
|
|
59
|
+
inputRate: r.input_rate ?? null,
|
|
60
|
+
outputRate: r.output_rate ?? null,
|
|
61
|
+
lastSeen: Number(r.last_seen ?? 0),
|
|
62
|
+
}));
|
|
63
|
+
const summary = {
|
|
64
|
+
totalRepos: repos.length,
|
|
65
|
+
totalCheckpoints: repos.reduce((a, r) => a + r.checkpointCount, 0),
|
|
66
|
+
totalTokensSaved: repos.reduce((a, r) => a + r.tokensSaved, 0),
|
|
67
|
+
totalCompressedOriginalBytes: repos.reduce((a, r) => a + r.compressedOriginalBytes, 0),
|
|
68
|
+
};
|
|
69
|
+
return { updatedAt: new Date().toISOString(), summary, repos };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
try {
|
|
76
|
+
db?.close();
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
16
81
|
// ---------------------------------------------------------------------------
|
|
17
82
|
// Helpers
|
|
18
83
|
// ---------------------------------------------------------------------------
|
|
@@ -111,6 +176,25 @@ function dashboardHtml(tierName) {
|
|
|
111
176
|
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
112
177
|
.empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
113
178
|
.offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
|
|
179
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
180
|
+
.tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
|
|
181
|
+
.tab:hover { color: #c9d1d9; border-color: #484f58; }
|
|
182
|
+
.tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
|
|
183
|
+
.tab-panel { display: none; }
|
|
184
|
+
.tab-panel.active { display: block; }
|
|
185
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
186
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
187
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
188
|
+
.summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
189
|
+
table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
|
|
190
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
191
|
+
table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
|
|
192
|
+
table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
|
|
193
|
+
table.repos tr:last-child td { border-bottom: none; }
|
|
194
|
+
table.repos tr:hover td { background: #1c2128; }
|
|
195
|
+
.repo-model { color: #a371f7; }
|
|
196
|
+
.repo-none { color: #484f58; font-style: italic; }
|
|
197
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
114
198
|
</style>
|
|
115
199
|
</head>
|
|
116
200
|
<body>
|
|
@@ -119,6 +203,14 @@ function dashboardHtml(tierName) {
|
|
|
119
203
|
|
|
120
204
|
<h1><span>mega-compact</span><span class="tier">${tierName}</span></h1>
|
|
121
205
|
|
|
206
|
+
<nav class="tabs">
|
|
207
|
+
<button class="tab active" data-tab="current">Current repo</button>
|
|
208
|
+
<button class="tab" data-tab="all">All repos</button>
|
|
209
|
+
<button class="tab" data-tab="summary">Summary</button>
|
|
210
|
+
</nav>
|
|
211
|
+
|
|
212
|
+
<!-- Current repo (existing single-repo view) -->
|
|
213
|
+
<div class="tab-panel" id="panel-current">
|
|
122
214
|
<div class="grid">
|
|
123
215
|
<div class="card">
|
|
124
216
|
<h2>Context Window</h2>
|
|
@@ -206,6 +298,35 @@ function dashboardHtml(tierName) {
|
|
|
206
298
|
</div>
|
|
207
299
|
|
|
208
300
|
<div class="updated" id="updated"></div>
|
|
301
|
+
</div><!-- /panel-current -->
|
|
302
|
+
|
|
303
|
+
<!-- All repos (machine-wide registry from index.sqlite) -->
|
|
304
|
+
<div class="tab-panel" id="panel-all">
|
|
305
|
+
<table class="repos">
|
|
306
|
+
<thead>
|
|
307
|
+
<tr>
|
|
308
|
+
<th>Repo</th><th>Model</th>
|
|
309
|
+
<th style="text-align:right">Checkpoints</th>
|
|
310
|
+
<th style="text-align:right">Tokens Saved</th>
|
|
311
|
+
<th style="text-align:right">Retained</th>
|
|
312
|
+
<th style="text-align:right">Last Compacted</th>
|
|
313
|
+
</tr>
|
|
314
|
+
</thead>
|
|
315
|
+
<tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
|
|
316
|
+
</table>
|
|
317
|
+
<div class="updated" id="all-updated"></div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- Summary (aggregate across all repos) -->
|
|
321
|
+
<div class="tab-panel" id="panel-summary">
|
|
322
|
+
<div class="summary-grid">
|
|
323
|
+
<div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
|
|
324
|
+
<div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
|
|
325
|
+
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
326
|
+
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="updated" id="sm-updated"></div>
|
|
329
|
+
</div>
|
|
209
330
|
|
|
210
331
|
<script>
|
|
211
332
|
(function() {
|
|
@@ -331,6 +452,69 @@ function dashboardHtml(tierName) {
|
|
|
331
452
|
};
|
|
332
453
|
}
|
|
333
454
|
connectSSE();
|
|
455
|
+
|
|
456
|
+
// --- Multi-repo (index.sqlite via /api/index) ---------------------------
|
|
457
|
+
function fmtBytesTop(b) {
|
|
458
|
+
b = b || 0;
|
|
459
|
+
if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
|
|
460
|
+
if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
|
|
461
|
+
return b + ' B';
|
|
462
|
+
}
|
|
463
|
+
function renderIndex(d) {
|
|
464
|
+
d = d || { updatedAt: null, summary: null, repos: [] };
|
|
465
|
+
var repos = d.repos || [];
|
|
466
|
+
var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
|
|
467
|
+
document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
|
|
468
|
+
document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
|
|
469
|
+
document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
|
|
470
|
+
document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
|
|
471
|
+
|
|
472
|
+
var body = document.getElementById('all-rows');
|
|
473
|
+
if (!repos.length) {
|
|
474
|
+
body.innerHTML = '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
|
|
475
|
+
} else {
|
|
476
|
+
body.innerHTML = repos.map(function(r) {
|
|
477
|
+
var model = r.modelName
|
|
478
|
+
? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
|
|
479
|
+
: '<span class="repo-none">—</span>';
|
|
480
|
+
var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
|
|
481
|
+
return '<tr>' +
|
|
482
|
+
'<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
|
|
483
|
+
'<td>' + model + '</td>' +
|
|
484
|
+
'<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
|
|
485
|
+
'<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
|
|
486
|
+
'<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
|
|
487
|
+
'<td class="num">' + sanitize(when) + '</td>' +
|
|
488
|
+
'</tr>';
|
|
489
|
+
}).join('');
|
|
490
|
+
}
|
|
491
|
+
var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
|
|
492
|
+
document.getElementById('all-updated').textContent = stamp;
|
|
493
|
+
document.getElementById('sm-updated').textContent = stamp;
|
|
494
|
+
}
|
|
495
|
+
function pollIndex() {
|
|
496
|
+
fetch('/api/index').then(function(r) { return r.json(); }).then(renderIndex).catch(function() {});
|
|
497
|
+
}
|
|
498
|
+
pollIndex();
|
|
499
|
+
setInterval(pollIndex, 5000);
|
|
500
|
+
|
|
501
|
+
// --- Tab switching ------------------------------------------------------
|
|
502
|
+
var tabs = document.querySelectorAll('.tab');
|
|
503
|
+
var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
|
|
504
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
505
|
+
tabs[i].addEventListener('click', function() {
|
|
506
|
+
var name = this.getAttribute('data-tab');
|
|
507
|
+
for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
|
|
508
|
+
this.classList.add('active');
|
|
509
|
+
for (var k in panels) {
|
|
510
|
+
if (Object.prototype.hasOwnProperty.call(panels, k)) {
|
|
511
|
+
var el = document.getElementById(panels[k]);
|
|
512
|
+
if (el) el.classList.toggle('active', k === name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (name === 'all' || name === 'summary') pollIndex();
|
|
516
|
+
});
|
|
517
|
+
}
|
|
334
518
|
})();
|
|
335
519
|
</script>
|
|
336
520
|
</body>
|
|
@@ -380,6 +564,14 @@ export function launchDashboardServer(stateDir) {
|
|
|
380
564
|
res.end(JSON.stringify(snap));
|
|
381
565
|
return;
|
|
382
566
|
}
|
|
567
|
+
// Multi-repo aggregate (Phase 5b): the machine-wide repo registry read
|
|
568
|
+
// directly from SQLite (index.sqlite). Lets one dashboard show every repo's
|
|
569
|
+
// checkpoints, tokens saved, and active model. Read-only.
|
|
570
|
+
if (req.url === "/api/index") {
|
|
571
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
572
|
+
res.end(JSON.stringify(readIndex() ?? { updatedAt: null, summary: null, repos: [] }));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
383
575
|
if (req.url === "/api/events") {
|
|
384
576
|
res.writeHead(200, {
|
|
385
577
|
"Content-Type": "text/event-stream",
|
|
@@ -14,7 +14,7 @@ import { VectorStore } from "../src/vectorStore.js";
|
|
|
14
14
|
import { toEngineMessages } from "../src/adapt.js";
|
|
15
15
|
import { normalizeSessionId } from "../src/store.js";
|
|
16
16
|
import { Logger } from "../src/log.js";
|
|
17
|
-
import { recordModelSnapshot } from "../src/store/sqlite.js";
|
|
17
|
+
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel } from "../src/store/sqlite.js";
|
|
18
18
|
import { repoStateDir, resolveRepoRoot } from "./mega-config.js";
|
|
19
19
|
import { Dashboard } from "./mega-dashboard.js";
|
|
20
20
|
export const STATUS_KEY = "mega-compact";
|
|
@@ -117,6 +117,27 @@ export class MegaRuntime {
|
|
|
117
117
|
this.store = new VectorStore({ dedupSim: this.config.dedupSim, stateDir: dir });
|
|
118
118
|
this.logger = new Logger({ enabled: this.config.debug, path: join(dir, "mega-compact.log") });
|
|
119
119
|
this.dashboard = new Dashboard(dir);
|
|
120
|
+
// Aggregate this repo into the machine-wide index so the multi-repo
|
|
121
|
+
// dashboard (Summary / All-repos tabs) can show it alongside every other
|
|
122
|
+
// repo. Best-effort + non-fatal: a read-only index dir or contention must
|
|
123
|
+
// never break the per-repo compaction path. Runs only on repo-switch
|
|
124
|
+
// (this branch), so it's infrequent — not per-context-event.
|
|
125
|
+
try {
|
|
126
|
+
const repo = this.store.repoStats();
|
|
127
|
+
const di = this.store.dataInvariant();
|
|
128
|
+
const root = key !== dir ? key : resolveRepoRoot(cwd ?? dir) ?? dir;
|
|
129
|
+
upsertRepoRegistry({
|
|
130
|
+
repoRoot: root,
|
|
131
|
+
displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
|
|
132
|
+
stateDir: dir,
|
|
133
|
+
checkpointCount: repo.checkpointCount,
|
|
134
|
+
tokensSaved: repo.tokensSaved,
|
|
135
|
+
compressedOriginalBytes: di.compressedOriginalBytes,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* non-fatal: index aggregation must not block compaction */
|
|
140
|
+
}
|
|
120
141
|
return dir;
|
|
121
142
|
}
|
|
122
143
|
// ---- dashboard snapshot + widget ------------------------------------------
|
|
@@ -127,6 +148,17 @@ export class MegaRuntime {
|
|
|
127
148
|
const st = this.store.stats(this.rt.sessionId);
|
|
128
149
|
const repo = this.store.repoStats();
|
|
129
150
|
const di = this.store.dataInvariant();
|
|
151
|
+
// Active model/provider for the current-repo card + the multi-repo table.
|
|
152
|
+
const modelSnap = latestModelSnapshot(this.currentStateDir);
|
|
153
|
+
const model = modelSnap
|
|
154
|
+
? {
|
|
155
|
+
name: modelSnap.modelName ?? modelSnap.modelId,
|
|
156
|
+
provider: modelSnap.provider,
|
|
157
|
+
providerName: modelSnap.providerName ?? "",
|
|
158
|
+
inputRate: modelSnap.inputRate,
|
|
159
|
+
outputRate: modelSnap.outputRate,
|
|
160
|
+
}
|
|
161
|
+
: undefined;
|
|
130
162
|
const armed = this.lastCtxPercent != null && this.lastCtxPercent >= this.config.fastGatePct;
|
|
131
163
|
const ready = armed && (this.lastCtxTokens ?? 0) >= this.config.thresholdTokens;
|
|
132
164
|
this.dashboard.snapshot({
|
|
@@ -171,6 +203,7 @@ export class MegaRuntime {
|
|
|
171
203
|
duplicatesCollapsed: di.duplicatesCollapsed,
|
|
172
204
|
bytesPermanentlyDeleted: di.bytesPermanentlyDeleted,
|
|
173
205
|
},
|
|
206
|
+
model,
|
|
174
207
|
});
|
|
175
208
|
// Live stats widget above the editor
|
|
176
209
|
if (ctx) {
|
|
@@ -301,6 +334,18 @@ export class MegaRuntime {
|
|
|
301
334
|
try {
|
|
302
335
|
const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
|
|
303
336
|
recordModelSnapshot(repo, snap, this.currentStateDir);
|
|
337
|
+
// Denormalize the active model into the machine-wide index so the
|
|
338
|
+
// All-repos dashboard table can show provider/model per repo without
|
|
339
|
+
// opening every repo's DB. Best-effort + non-fatal.
|
|
340
|
+
recordRepoModel(repo, {
|
|
341
|
+
provider: snap.provider,
|
|
342
|
+
providerName: snap.providerName,
|
|
343
|
+
modelName: snap.modelName,
|
|
344
|
+
inputRate: snap.inputRate,
|
|
345
|
+
outputRate: snap.outputRate,
|
|
346
|
+
stateDir: this.currentStateDir,
|
|
347
|
+
displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
|
|
348
|
+
});
|
|
304
349
|
}
|
|
305
350
|
catch { /* non-fatal: cost estimation degrades to model-in-memory only */ }
|
|
306
351
|
}
|
package/dist/src/store/sqlite.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import Database from "better-sqlite3";
|
|
18
18
|
import { existsSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { homedir, tmpdir } from "node:os";
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import { getStateDir } from "../store.js";
|
|
21
22
|
import { normalizeSessionId } from "../store.js";
|
|
@@ -58,6 +59,167 @@ export function openStore(stateDir = getStateDir()) {
|
|
|
58
59
|
cache.set(stateDir, db);
|
|
59
60
|
return db;
|
|
60
61
|
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Global machine-wide index (Phase 5b): a single SQLite DB, separate from every
|
|
64
|
+
// per-repo store, that aggregates one row per repo this machine has run on. The
|
|
65
|
+
// multi-repo dashboard (Summary / All-repos tabs) reads it so ONE dashboard can
|
|
66
|
+
// show every repo's checkpoints, tokens saved, and active model — instead of a
|
|
67
|
+
// per-repo dashboard that only ever sees the repo it was launched from.
|
|
68
|
+
//
|
|
69
|
+
// Written by every pi process on repo-switch (bindRepo) + model capture; read by
|
|
70
|
+
// the dashboard server. Concurrency across 10+ pi processes is handled by WAL +
|
|
71
|
+
// infrequent idempotent upserts (ON CONFLICT). Fully local (PREVENT-PI-004).
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/** Resolve the machine-wide index directory (env-overridable). */
|
|
74
|
+
export function getIndexDir() {
|
|
75
|
+
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
76
|
+
if (override && override.trim() !== "")
|
|
77
|
+
return override;
|
|
78
|
+
// homedir() can throw in exotic sandboxes; fall back to tmpdir.
|
|
79
|
+
try {
|
|
80
|
+
return join(homedir(), ".mega-compact-index");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return join(tmpdir(), ".mega-compact-index");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let indexCache;
|
|
87
|
+
let indexCacheDir;
|
|
88
|
+
/** Open (or reuse) the machine-wide index DB. WAL for concurrent writers. */
|
|
89
|
+
export function openIndexStore(indexDir = getIndexDir()) {
|
|
90
|
+
if (indexCache && indexCacheDir === indexDir)
|
|
91
|
+
return indexCache;
|
|
92
|
+
if (!existsSync(indexDir))
|
|
93
|
+
mkdirSync(indexDir, { recursive: true });
|
|
94
|
+
const db = new Database(join(indexDir, "index.sqlite"));
|
|
95
|
+
db.pragma("journal_mode = WAL");
|
|
96
|
+
db.pragma("busy_timeout = 3000"); // tolerate brief cross-process write contention
|
|
97
|
+
db.exec(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS repo_registry (
|
|
99
|
+
repo_root TEXT PRIMARY KEY,
|
|
100
|
+
display_name TEXT,
|
|
101
|
+
state_dir TEXT NOT NULL,
|
|
102
|
+
first_seen INTEGER,
|
|
103
|
+
last_seen INTEGER,
|
|
104
|
+
last_compacted_at INTEGER,
|
|
105
|
+
checkpoint_count INTEGER DEFAULT 0,
|
|
106
|
+
tokens_saved INTEGER DEFAULT 0,
|
|
107
|
+
compressed_original_bytes INTEGER DEFAULT 0,
|
|
108
|
+
provider TEXT,
|
|
109
|
+
provider_name TEXT,
|
|
110
|
+
model_name TEXT,
|
|
111
|
+
input_rate REAL,
|
|
112
|
+
output_rate REAL,
|
|
113
|
+
model_captured_at INTEGER
|
|
114
|
+
);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_registry_last_seen ON repo_registry(last_seen DESC);
|
|
116
|
+
`);
|
|
117
|
+
indexCache = db;
|
|
118
|
+
indexCacheDir = indexDir;
|
|
119
|
+
return db;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Upsert a repo's aggregate stats into the global index. Called on repo-switch
|
|
123
|
+
* (infrequent). Preserves first_seen + the model columns on update (model is
|
|
124
|
+
* written separately by recordRepoModel so we never clobber it here with nulls).
|
|
125
|
+
*/
|
|
126
|
+
export function upsertRepoRegistry(row, indexDir = getIndexDir()) {
|
|
127
|
+
const db = openIndexStore(indexDir);
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
db.prepare(`INSERT INTO repo_registry
|
|
130
|
+
(repo_root, display_name, state_dir, first_seen, last_seen, last_compacted_at,
|
|
131
|
+
checkpoint_count, tokens_saved, compressed_original_bytes)
|
|
132
|
+
VALUES (@repo_root, @display_name, @state_dir, @now, @now, @last_compacted_at,
|
|
133
|
+
@checkpoint_count, @tokens_saved, @compressed_original_bytes)
|
|
134
|
+
ON CONFLICT(repo_root) DO UPDATE SET
|
|
135
|
+
display_name = excluded.display_name,
|
|
136
|
+
state_dir = excluded.state_dir,
|
|
137
|
+
last_seen = excluded.last_seen,
|
|
138
|
+
last_compacted_at = COALESCE(excluded.last_compacted_at, repo_registry.last_compacted_at),
|
|
139
|
+
checkpoint_count = excluded.checkpoint_count,
|
|
140
|
+
tokens_saved = excluded.tokens_saved,
|
|
141
|
+
compressed_original_bytes = excluded.compressed_original_bytes`).run({
|
|
142
|
+
repo_root: row.repoRoot,
|
|
143
|
+
display_name: row.displayName,
|
|
144
|
+
state_dir: row.stateDir,
|
|
145
|
+
now,
|
|
146
|
+
last_compacted_at: row.lastCompactedAt ?? null,
|
|
147
|
+
checkpoint_count: row.checkpointCount,
|
|
148
|
+
tokens_saved: row.tokensSaved,
|
|
149
|
+
compressed_original_bytes: row.compressedOriginalBytes,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Record the active model/provider for a repo in the global index (denormalized
|
|
154
|
+
* so the All-repos table shows model without opening each repo's DB). Upserts a
|
|
155
|
+
* bare registry row if the repo isn't registered yet.
|
|
156
|
+
*/
|
|
157
|
+
export function recordRepoModel(repoRoot, model, indexDir = getIndexDir()) {
|
|
158
|
+
const db = openIndexStore(indexDir);
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
db.prepare(`INSERT INTO repo_registry
|
|
161
|
+
(repo_root, display_name, state_dir, first_seen, last_seen,
|
|
162
|
+
provider, provider_name, model_name, input_rate, output_rate, model_captured_at)
|
|
163
|
+
VALUES (@repo_root, @display_name, @state_dir, @now, @now,
|
|
164
|
+
@provider, @provider_name, @model_name, @input_rate, @output_rate, @now)
|
|
165
|
+
ON CONFLICT(repo_root) DO UPDATE SET
|
|
166
|
+
last_seen = excluded.last_seen,
|
|
167
|
+
provider = excluded.provider,
|
|
168
|
+
provider_name = excluded.provider_name,
|
|
169
|
+
model_name = excluded.model_name,
|
|
170
|
+
input_rate = excluded.input_rate,
|
|
171
|
+
output_rate = excluded.output_rate,
|
|
172
|
+
model_captured_at = excluded.model_captured_at`).run({
|
|
173
|
+
repo_root: repoRoot,
|
|
174
|
+
display_name: model.displayName,
|
|
175
|
+
state_dir: model.stateDir,
|
|
176
|
+
now,
|
|
177
|
+
provider: model.provider,
|
|
178
|
+
provider_name: model.providerName,
|
|
179
|
+
model_name: model.modelName,
|
|
180
|
+
input_rate: model.inputRate,
|
|
181
|
+
output_rate: model.outputRate,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function mapRegistryRow(row) {
|
|
185
|
+
return {
|
|
186
|
+
repoRoot: row.repo_root,
|
|
187
|
+
displayName: row.display_name ?? "",
|
|
188
|
+
stateDir: row.state_dir,
|
|
189
|
+
firstSeen: row.first_seen ?? 0,
|
|
190
|
+
lastSeen: row.last_seen ?? 0,
|
|
191
|
+
lastCompactedAt: row.last_compacted_at ?? null,
|
|
192
|
+
checkpointCount: row.checkpoint_count ?? 0,
|
|
193
|
+
tokensSaved: row.tokens_saved ?? 0,
|
|
194
|
+
compressedOriginalBytes: row.compressed_original_bytes ?? 0,
|
|
195
|
+
provider: row.provider ?? null,
|
|
196
|
+
providerName: row.provider_name ?? null,
|
|
197
|
+
modelName: row.model_name ?? null,
|
|
198
|
+
inputRate: row.input_rate ?? null,
|
|
199
|
+
outputRate: row.output_rate ?? null,
|
|
200
|
+
modelCapturedAt: row.model_captured_at ?? null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** All registered repos, most-recently-seen first. */
|
|
204
|
+
export function listRepoRegistry(indexDir = getIndexDir()) {
|
|
205
|
+
const db = openIndexStore(indexDir);
|
|
206
|
+
const rows = db.prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC").all();
|
|
207
|
+
return rows.map(mapRegistryRow);
|
|
208
|
+
}
|
|
209
|
+
/** A single repo's registry row, or undefined. */
|
|
210
|
+
export function getRepoRegistry(repoRoot, indexDir = getIndexDir()) {
|
|
211
|
+
const db = openIndexStore(indexDir);
|
|
212
|
+
const row = db.prepare("SELECT * FROM repo_registry WHERE repo_root = ?").get(repoRoot);
|
|
213
|
+
return row ? mapRegistryRow(row) : undefined;
|
|
214
|
+
}
|
|
215
|
+
/** Close the cached index connection (test teardown only). */
|
|
216
|
+
export function closeIndexStore() {
|
|
217
|
+
if (indexCache) {
|
|
218
|
+
indexCache.close();
|
|
219
|
+
indexCache = undefined;
|
|
220
|
+
indexCacheDir = undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
61
223
|
function initSchema(db) {
|
|
62
224
|
db.exec(`
|
|
63
225
|
CREATE TABLE IF NOT EXISTS context_chunks (
|
package/extensions/DASHBOARD.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight local web dashboard for monitoring mega-compact's live state — compactions, context usage, checkpoints, and recall hits.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Uses Node built-in modules (`http`, `fs`, `path`) plus `better-sqlite3` (the
|
|
6
|
+
project's one-store DB backend) to read the machine-wide multi-repo index.
|
|
6
7
|
|
|
7
8
|
## Quick Start
|
|
8
9
|
|
|
@@ -72,8 +73,8 @@ All files are written to the extension's state directory
|
|
|
72
73
|
The server runs as a detached child process, independent of the pi session. It:
|
|
73
74
|
- Auto-discovers the state directory from the `port.pid` file
|
|
74
75
|
- Cleans up stale `port.pid` files from dead processes
|
|
75
|
-
- Supports `
|
|
76
|
-
- Serves static HTML
|
|
76
|
+
- Supports `SIGTERM`/`SIGINT` for graceful shutdown
|
|
77
|
+
- Serves static HTML; reads the multi-repo index from SQLite (`better-sqlite3`)
|
|
77
78
|
|
|
78
79
|
## Browser UI
|
|
79
80
|
|
|
@@ -147,11 +148,23 @@ The extension tracks active sub-agents in real-time:
|
|
|
147
148
|
- The server only listens on `127.0.0.1` (localhost)
|
|
148
149
|
- No authentication (local-only, not exposed to network)
|
|
149
150
|
- No write endpoints — all APIs are read-only
|
|
150
|
-
|
|
151
|
+
|
|
152
|
+
## Multi-Repo Index (Phase 5b)
|
|
153
|
+
|
|
154
|
+
The dashboard shows every repo that has run mega-compact on this machine, not
|
|
155
|
+
just the one it was launched from. The extension writes a machine-wide
|
|
156
|
+
`repo_registry` into a single SQLite DB (`<indexDir>/index.sqlite`, where
|
|
157
|
+
`indexDir` is `$MEGACOMPACT_INDEX_DIR` or `~/.mega-compact-index`), one row per
|
|
158
|
+
repo with checkpoint count, tokens saved, compressed-original bytes, and the
|
|
159
|
+
active model/provider (denormalized from `model_snapshots`). The dashboard
|
|
160
|
+
server opens that table read-only (`GET /api/index`) and renders the **All
|
|
161
|
+
repos** (per-repo table) and **Summary** (machine-wide aggregate) tabs. All
|
|
162
|
+
registry data lives in SQLite — there is no JSON mirror; the "one store"
|
|
163
|
+
invariant is preserved end-to-end.
|
|
151
164
|
|
|
152
165
|
## Troubleshooting
|
|
153
166
|
|
|
154
|
-
**Port already in use**: The server picks a
|
|
167
|
+
**Port already in use**: The server picks a port in 9320–9329. If all are taken, it will retry. Check `/dashboard-status` for the current port.
|
|
155
168
|
|
|
156
169
|
**Server won't start**: Check for stale `port.pid` files. Run `/dashboard-stop` to clean up, then try `/dashboard` again.
|
|
157
170
|
|
|
@@ -13,7 +13,81 @@
|
|
|
13
13
|
|
|
14
14
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
16
17
|
import { join } from "node:path";
|
|
18
|
+
import Database from "better-sqlite3";
|
|
19
|
+
|
|
20
|
+
// --- Multi-repo index (Phase 5b) ------------------------------------------------
|
|
21
|
+
// The extension writes a machine-wide repo registry into a single SQLite DB
|
|
22
|
+
// (<indexDir>/index.sqlite) as the concurrency-safe write path; the dashboard
|
|
23
|
+
// reads that table directly (one read-only connection, opened per request so a
|
|
24
|
+
// concurrent writer's WAL never blocks the request). All registry data lives in
|
|
25
|
+
// SQLite (the project's one-store invariant) — there is no JSON mirror. Same
|
|
26
|
+
// index-dir resolution as src/store/sqlite.ts getIndexDir().
|
|
27
|
+
function getIndexDir(): string {
|
|
28
|
+
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
29
|
+
if (override && override.trim() !== "") return override;
|
|
30
|
+
try {
|
|
31
|
+
return join(homedir(), ".mega-compact-index");
|
|
32
|
+
} catch {
|
|
33
|
+
return join("/tmp", ".mega-compact-index");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IndexRepo {
|
|
38
|
+
repoRoot: string;
|
|
39
|
+
displayName: string;
|
|
40
|
+
checkpointCount: number;
|
|
41
|
+
tokensSaved: number;
|
|
42
|
+
compressedOriginalBytes: number;
|
|
43
|
+
lastCompactedAt: number | null;
|
|
44
|
+
provider: string | null;
|
|
45
|
+
providerName: string | null;
|
|
46
|
+
modelName: string | null;
|
|
47
|
+
inputRate: number | null;
|
|
48
|
+
outputRate: number | null;
|
|
49
|
+
lastSeen: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read the machine-wide repo registry from SQLite (read-only, single shot). */
|
|
53
|
+
function readIndex(): { updatedAt: string; summary: unknown; repos: unknown[] } | null {
|
|
54
|
+
const indexPath = join(getIndexDir(), "index.sqlite");
|
|
55
|
+
if (!existsSync(indexPath)) return null;
|
|
56
|
+
let db: Database.Database | undefined;
|
|
57
|
+
try {
|
|
58
|
+
// Read-only + immutable WAL so a concurrent writer's WAL never blocks us.
|
|
59
|
+
db = new Database(indexPath, { readonly: true, fileMustExist: true });
|
|
60
|
+
db.pragma("journal_mode = WAL");
|
|
61
|
+
const rows = db
|
|
62
|
+
.prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
|
|
63
|
+
.all() as Record<string, unknown>[];
|
|
64
|
+
const repos: IndexRepo[] = rows.map((r) => ({
|
|
65
|
+
repoRoot: String(r.repo_root ?? ""),
|
|
66
|
+
displayName: String(r.display_name ?? ""),
|
|
67
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
68
|
+
tokensSaved: Number(r.tokens_saved ?? 0),
|
|
69
|
+
compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
|
|
70
|
+
lastCompactedAt: (r.last_compacted_at as number | null) ?? null,
|
|
71
|
+
provider: (r.provider as string | null) ?? null,
|
|
72
|
+
providerName: (r.provider_name as string | null) ?? null,
|
|
73
|
+
modelName: (r.model_name as string | null) ?? null,
|
|
74
|
+
inputRate: (r.input_rate as number | null) ?? null,
|
|
75
|
+
outputRate: (r.output_rate as number | null) ?? null,
|
|
76
|
+
lastSeen: Number(r.last_seen ?? 0),
|
|
77
|
+
}));
|
|
78
|
+
const summary = {
|
|
79
|
+
totalRepos: repos.length,
|
|
80
|
+
totalCheckpoints: repos.reduce((a, r) => a + r.checkpointCount, 0),
|
|
81
|
+
totalTokensSaved: repos.reduce((a, r) => a + r.tokensSaved, 0),
|
|
82
|
+
totalCompressedOriginalBytes: repos.reduce((a, r) => a + r.compressedOriginalBytes, 0),
|
|
83
|
+
};
|
|
84
|
+
return { updatedAt: new Date().toISOString(), summary, repos };
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
} finally {
|
|
88
|
+
try { db?.close(); } catch { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
17
91
|
|
|
18
92
|
// ---------------------------------------------------------------------------
|
|
19
93
|
// Types
|
|
@@ -175,6 +249,25 @@ function dashboardHtml(tierName: string): string {
|
|
|
175
249
|
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
176
250
|
.empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
177
251
|
.offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
|
|
252
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
253
|
+
.tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
|
|
254
|
+
.tab:hover { color: #c9d1d9; border-color: #484f58; }
|
|
255
|
+
.tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
|
|
256
|
+
.tab-panel { display: none; }
|
|
257
|
+
.tab-panel.active { display: block; }
|
|
258
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
259
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
260
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
261
|
+
.summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
262
|
+
table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
|
|
263
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
264
|
+
table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
|
|
265
|
+
table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
|
|
266
|
+
table.repos tr:last-child td { border-bottom: none; }
|
|
267
|
+
table.repos tr:hover td { background: #1c2128; }
|
|
268
|
+
.repo-model { color: #a371f7; }
|
|
269
|
+
.repo-none { color: #484f58; font-style: italic; }
|
|
270
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
178
271
|
</style>
|
|
179
272
|
</head>
|
|
180
273
|
<body>
|
|
@@ -183,6 +276,14 @@ function dashboardHtml(tierName: string): string {
|
|
|
183
276
|
|
|
184
277
|
<h1><span>mega-compact</span><span class="tier">${tierName}</span></h1>
|
|
185
278
|
|
|
279
|
+
<nav class="tabs">
|
|
280
|
+
<button class="tab active" data-tab="current">Current repo</button>
|
|
281
|
+
<button class="tab" data-tab="all">All repos</button>
|
|
282
|
+
<button class="tab" data-tab="summary">Summary</button>
|
|
283
|
+
</nav>
|
|
284
|
+
|
|
285
|
+
<!-- Current repo (existing single-repo view) -->
|
|
286
|
+
<div class="tab-panel" id="panel-current">
|
|
186
287
|
<div class="grid">
|
|
187
288
|
<div class="card">
|
|
188
289
|
<h2>Context Window</h2>
|
|
@@ -270,6 +371,35 @@ function dashboardHtml(tierName: string): string {
|
|
|
270
371
|
</div>
|
|
271
372
|
|
|
272
373
|
<div class="updated" id="updated"></div>
|
|
374
|
+
</div><!-- /panel-current -->
|
|
375
|
+
|
|
376
|
+
<!-- All repos (machine-wide registry from index.sqlite) -->
|
|
377
|
+
<div class="tab-panel" id="panel-all">
|
|
378
|
+
<table class="repos">
|
|
379
|
+
<thead>
|
|
380
|
+
<tr>
|
|
381
|
+
<th>Repo</th><th>Model</th>
|
|
382
|
+
<th style="text-align:right">Checkpoints</th>
|
|
383
|
+
<th style="text-align:right">Tokens Saved</th>
|
|
384
|
+
<th style="text-align:right">Retained</th>
|
|
385
|
+
<th style="text-align:right">Last Compacted</th>
|
|
386
|
+
</tr>
|
|
387
|
+
</thead>
|
|
388
|
+
<tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
|
|
389
|
+
</table>
|
|
390
|
+
<div class="updated" id="all-updated"></div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<!-- Summary (aggregate across all repos) -->
|
|
394
|
+
<div class="tab-panel" id="panel-summary">
|
|
395
|
+
<div class="summary-grid">
|
|
396
|
+
<div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
|
|
397
|
+
<div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
|
|
398
|
+
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
399
|
+
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="updated" id="sm-updated"></div>
|
|
402
|
+
</div>
|
|
273
403
|
|
|
274
404
|
<script>
|
|
275
405
|
(function() {
|
|
@@ -395,6 +525,69 @@ function dashboardHtml(tierName: string): string {
|
|
|
395
525
|
};
|
|
396
526
|
}
|
|
397
527
|
connectSSE();
|
|
528
|
+
|
|
529
|
+
// --- Multi-repo (index.sqlite via /api/index) ---------------------------
|
|
530
|
+
function fmtBytesTop(b) {
|
|
531
|
+
b = b || 0;
|
|
532
|
+
if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
|
|
533
|
+
if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
|
|
534
|
+
return b + ' B';
|
|
535
|
+
}
|
|
536
|
+
function renderIndex(d) {
|
|
537
|
+
d = d || { updatedAt: null, summary: null, repos: [] };
|
|
538
|
+
var repos = d.repos || [];
|
|
539
|
+
var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
|
|
540
|
+
document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
|
|
541
|
+
document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
|
|
542
|
+
document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
|
|
543
|
+
document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
|
|
544
|
+
|
|
545
|
+
var body = document.getElementById('all-rows');
|
|
546
|
+
if (!repos.length) {
|
|
547
|
+
body.innerHTML = '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
|
|
548
|
+
} else {
|
|
549
|
+
body.innerHTML = repos.map(function(r) {
|
|
550
|
+
var model = r.modelName
|
|
551
|
+
? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
|
|
552
|
+
: '<span class="repo-none">—</span>';
|
|
553
|
+
var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
|
|
554
|
+
return '<tr>' +
|
|
555
|
+
'<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
|
|
556
|
+
'<td>' + model + '</td>' +
|
|
557
|
+
'<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
|
|
558
|
+
'<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
|
|
559
|
+
'<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
|
|
560
|
+
'<td class="num">' + sanitize(when) + '</td>' +
|
|
561
|
+
'</tr>';
|
|
562
|
+
}).join('');
|
|
563
|
+
}
|
|
564
|
+
var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
|
|
565
|
+
document.getElementById('all-updated').textContent = stamp;
|
|
566
|
+
document.getElementById('sm-updated').textContent = stamp;
|
|
567
|
+
}
|
|
568
|
+
function pollIndex() {
|
|
569
|
+
fetch('/api/index').then(function(r) { return r.json(); }).then(renderIndex).catch(function() {});
|
|
570
|
+
}
|
|
571
|
+
pollIndex();
|
|
572
|
+
setInterval(pollIndex, 5000);
|
|
573
|
+
|
|
574
|
+
// --- Tab switching ------------------------------------------------------
|
|
575
|
+
var tabs = document.querySelectorAll('.tab');
|
|
576
|
+
var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
|
|
577
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
578
|
+
tabs[i].addEventListener('click', function() {
|
|
579
|
+
var name = this.getAttribute('data-tab');
|
|
580
|
+
for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
|
|
581
|
+
this.classList.add('active');
|
|
582
|
+
for (var k in panels) {
|
|
583
|
+
if (Object.prototype.hasOwnProperty.call(panels, k)) {
|
|
584
|
+
var el = document.getElementById(panels[k]);
|
|
585
|
+
if (el) el.classList.toggle('active', k === name);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (name === 'all' || name === 'summary') pollIndex();
|
|
589
|
+
});
|
|
590
|
+
}
|
|
398
591
|
})();
|
|
399
592
|
</script>
|
|
400
593
|
</body>
|
|
@@ -453,6 +646,15 @@ export function launchDashboardServer(stateDir: string): Promise<{ port: number;
|
|
|
453
646
|
return;
|
|
454
647
|
}
|
|
455
648
|
|
|
649
|
+
// Multi-repo aggregate (Phase 5b): the machine-wide repo registry read
|
|
650
|
+
// directly from SQLite (index.sqlite). Lets one dashboard show every repo's
|
|
651
|
+
// checkpoints, tokens saved, and active model. Read-only.
|
|
652
|
+
if (req.url === "/api/index") {
|
|
653
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
654
|
+
res.end(JSON.stringify(readIndex() ?? { updatedAt: null, summary: null, repos: [] }));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
456
658
|
if (req.url === "/api/events") {
|
|
457
659
|
res.writeHead(200, {
|
|
458
660
|
"Content-Type": "text/event-stream",
|
|
@@ -82,6 +82,14 @@ export interface DashboardSnapshot {
|
|
|
82
82
|
duplicatesCollapsed: number; // dedup duplicates (original kept on survivor)
|
|
83
83
|
bytesPermanentlyDeleted: number; // ALWAYS 0 — the invariant
|
|
84
84
|
};
|
|
85
|
+
/** Active model/provider (captured live) — shown on the current-repo card. */
|
|
86
|
+
model?: {
|
|
87
|
+
name: string; // Model.name or Model.id
|
|
88
|
+
provider: string; // ProviderId (Model.provider)
|
|
89
|
+
providerName: string; // human display name (e.g. "OpenAI")
|
|
90
|
+
inputRate: number; // USD per input token (Model.cost)
|
|
91
|
+
outputRate: number; // USD per output token (Model.cost)
|
|
92
|
+
};
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
export class Dashboard {
|
|
@@ -17,7 +17,7 @@ import { VectorStore } from "../src/vectorStore.js";
|
|
|
17
17
|
import { toEngineMessages } from "../src/adapt.js";
|
|
18
18
|
import { normalizeSessionId } from "../src/store.js";
|
|
19
19
|
import { Logger } from "../src/log.js";
|
|
20
|
-
import { recordModelSnapshot, type ModelSnapshot } from "../src/store/sqlite.js";
|
|
20
|
+
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, type ModelSnapshot } from "../src/store/sqlite.js";
|
|
21
21
|
import { repoStateDir, resolveRepoRoot, type MegaConfig } from "./mega-config.js";
|
|
22
22
|
import { Dashboard, type DashboardSnapshot } from "./mega-dashboard.js";
|
|
23
23
|
|
|
@@ -142,6 +142,26 @@ export class MegaRuntime {
|
|
|
142
142
|
this.store = new VectorStore({ dedupSim: this.config.dedupSim, stateDir: dir });
|
|
143
143
|
this.logger = new Logger({ enabled: this.config.debug, path: join(dir, "mega-compact.log") });
|
|
144
144
|
this.dashboard = new Dashboard(dir);
|
|
145
|
+
// Aggregate this repo into the machine-wide index so the multi-repo
|
|
146
|
+
// dashboard (Summary / All-repos tabs) can show it alongside every other
|
|
147
|
+
// repo. Best-effort + non-fatal: a read-only index dir or contention must
|
|
148
|
+
// never break the per-repo compaction path. Runs only on repo-switch
|
|
149
|
+
// (this branch), so it's infrequent — not per-context-event.
|
|
150
|
+
try {
|
|
151
|
+
const repo = this.store.repoStats();
|
|
152
|
+
const di = this.store.dataInvariant();
|
|
153
|
+
const root = key !== dir ? key : resolveRepoRoot(cwd ?? dir) ?? dir;
|
|
154
|
+
upsertRepoRegistry({
|
|
155
|
+
repoRoot: root,
|
|
156
|
+
displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
|
|
157
|
+
stateDir: dir,
|
|
158
|
+
checkpointCount: repo.checkpointCount,
|
|
159
|
+
tokensSaved: repo.tokensSaved,
|
|
160
|
+
compressedOriginalBytes: di.compressedOriginalBytes,
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
/* non-fatal: index aggregation must not block compaction */
|
|
164
|
+
}
|
|
145
165
|
return dir;
|
|
146
166
|
}
|
|
147
167
|
|
|
@@ -153,6 +173,17 @@ export class MegaRuntime {
|
|
|
153
173
|
const st = this.store.stats(this.rt.sessionId);
|
|
154
174
|
const repo = this.store.repoStats();
|
|
155
175
|
const di = this.store.dataInvariant();
|
|
176
|
+
// Active model/provider for the current-repo card + the multi-repo table.
|
|
177
|
+
const modelSnap = latestModelSnapshot(this.currentStateDir);
|
|
178
|
+
const model = modelSnap
|
|
179
|
+
? {
|
|
180
|
+
name: modelSnap.modelName ?? modelSnap.modelId,
|
|
181
|
+
provider: modelSnap.provider,
|
|
182
|
+
providerName: modelSnap.providerName ?? "",
|
|
183
|
+
inputRate: modelSnap.inputRate,
|
|
184
|
+
outputRate: modelSnap.outputRate,
|
|
185
|
+
}
|
|
186
|
+
: undefined;
|
|
156
187
|
const armed = this.lastCtxPercent != null && this.lastCtxPercent >= this.config.fastGatePct;
|
|
157
188
|
const ready = armed && (this.lastCtxTokens ?? 0) >= this.config.thresholdTokens;
|
|
158
189
|
this.dashboard.snapshot({
|
|
@@ -197,6 +228,7 @@ export class MegaRuntime {
|
|
|
197
228
|
duplicatesCollapsed: di.duplicatesCollapsed,
|
|
198
229
|
bytesPermanentlyDeleted: di.bytesPermanentlyDeleted,
|
|
199
230
|
},
|
|
231
|
+
model,
|
|
200
232
|
} as DashboardSnapshot);
|
|
201
233
|
|
|
202
234
|
// Live stats widget above the editor
|
|
@@ -321,6 +353,18 @@ export class MegaRuntime {
|
|
|
321
353
|
try {
|
|
322
354
|
const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
|
|
323
355
|
recordModelSnapshot(repo, snap, this.currentStateDir);
|
|
356
|
+
// Denormalize the active model into the machine-wide index so the
|
|
357
|
+
// All-repos dashboard table can show provider/model per repo without
|
|
358
|
+
// opening every repo's DB. Best-effort + non-fatal.
|
|
359
|
+
recordRepoModel(repo, {
|
|
360
|
+
provider: snap.provider,
|
|
361
|
+
providerName: snap.providerName,
|
|
362
|
+
modelName: snap.modelName,
|
|
363
|
+
inputRate: snap.inputRate,
|
|
364
|
+
outputRate: snap.outputRate,
|
|
365
|
+
stateDir: this.currentStateDir,
|
|
366
|
+
displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
|
|
367
|
+
});
|
|
324
368
|
} catch { /* non-fatal: cost estimation degrades to model-in-memory only */ }
|
|
325
369
|
}
|
|
326
370
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
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-2-Clause",
|
package/src/store/sqlite.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import Database from "better-sqlite3";
|
|
19
19
|
import { existsSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { homedir, tmpdir } from "node:os";
|
|
20
21
|
import { join } from "node:path";
|
|
21
22
|
import { getStateDir } from "../store.js";
|
|
22
23
|
import type { StoredCheckpoint, SessionState } from "../store.js";
|
|
@@ -62,6 +63,219 @@ export function openStore(stateDir: string = getStateDir()): Database.Database {
|
|
|
62
63
|
return db;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Global machine-wide index (Phase 5b): a single SQLite DB, separate from every
|
|
68
|
+
// per-repo store, that aggregates one row per repo this machine has run on. The
|
|
69
|
+
// multi-repo dashboard (Summary / All-repos tabs) reads it so ONE dashboard can
|
|
70
|
+
// show every repo's checkpoints, tokens saved, and active model — instead of a
|
|
71
|
+
// per-repo dashboard that only ever sees the repo it was launched from.
|
|
72
|
+
//
|
|
73
|
+
// Written by every pi process on repo-switch (bindRepo) + model capture; read by
|
|
74
|
+
// the dashboard server. Concurrency across 10+ pi processes is handled by WAL +
|
|
75
|
+
// infrequent idempotent upserts (ON CONFLICT). Fully local (PREVENT-PI-004).
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Resolve the machine-wide index directory (env-overridable). */
|
|
79
|
+
export function getIndexDir(): string {
|
|
80
|
+
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
81
|
+
if (override && override.trim() !== "") return override;
|
|
82
|
+
// homedir() can throw in exotic sandboxes; fall back to tmpdir.
|
|
83
|
+
try {
|
|
84
|
+
return join(homedir(), ".mega-compact-index");
|
|
85
|
+
} catch {
|
|
86
|
+
return join(tmpdir(), ".mega-compact-index");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let indexCache: Database.Database | undefined;
|
|
91
|
+
let indexCacheDir: string | undefined;
|
|
92
|
+
|
|
93
|
+
/** Open (or reuse) the machine-wide index DB. WAL for concurrent writers. */
|
|
94
|
+
export function openIndexStore(indexDir: string = getIndexDir()): Database.Database {
|
|
95
|
+
if (indexCache && indexCacheDir === indexDir) return indexCache;
|
|
96
|
+
if (!existsSync(indexDir)) mkdirSync(indexDir, { recursive: true });
|
|
97
|
+
const db = new Database(join(indexDir, "index.sqlite"));
|
|
98
|
+
db.pragma("journal_mode = WAL");
|
|
99
|
+
db.pragma("busy_timeout = 3000"); // tolerate brief cross-process write contention
|
|
100
|
+
db.exec(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS repo_registry (
|
|
102
|
+
repo_root TEXT PRIMARY KEY,
|
|
103
|
+
display_name TEXT,
|
|
104
|
+
state_dir TEXT NOT NULL,
|
|
105
|
+
first_seen INTEGER,
|
|
106
|
+
last_seen INTEGER,
|
|
107
|
+
last_compacted_at INTEGER,
|
|
108
|
+
checkpoint_count INTEGER DEFAULT 0,
|
|
109
|
+
tokens_saved INTEGER DEFAULT 0,
|
|
110
|
+
compressed_original_bytes INTEGER DEFAULT 0,
|
|
111
|
+
provider TEXT,
|
|
112
|
+
provider_name TEXT,
|
|
113
|
+
model_name TEXT,
|
|
114
|
+
input_rate REAL,
|
|
115
|
+
output_rate REAL,
|
|
116
|
+
model_captured_at INTEGER
|
|
117
|
+
);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_registry_last_seen ON repo_registry(last_seen DESC);
|
|
119
|
+
`);
|
|
120
|
+
indexCache = db;
|
|
121
|
+
indexCacheDir = indexDir;
|
|
122
|
+
return db;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One row of the global repo registry (multi-repo dashboard source). */
|
|
126
|
+
export interface RepoRegistryRow {
|
|
127
|
+
repoRoot: string;
|
|
128
|
+
displayName: string;
|
|
129
|
+
stateDir: string;
|
|
130
|
+
firstSeen: number;
|
|
131
|
+
lastSeen: number;
|
|
132
|
+
lastCompactedAt: number | null;
|
|
133
|
+
checkpointCount: number;
|
|
134
|
+
tokensSaved: number;
|
|
135
|
+
compressedOriginalBytes: number;
|
|
136
|
+
provider: string | null;
|
|
137
|
+
providerName: string | null;
|
|
138
|
+
modelName: string | null;
|
|
139
|
+
inputRate: number | null;
|
|
140
|
+
outputRate: number | null;
|
|
141
|
+
modelCapturedAt: number | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Upsert a repo's aggregate stats into the global index. Called on repo-switch
|
|
146
|
+
* (infrequent). Preserves first_seen + the model columns on update (model is
|
|
147
|
+
* written separately by recordRepoModel so we never clobber it here with nulls).
|
|
148
|
+
*/
|
|
149
|
+
export function upsertRepoRegistry(
|
|
150
|
+
row: {
|
|
151
|
+
repoRoot: string;
|
|
152
|
+
displayName: string;
|
|
153
|
+
stateDir: string;
|
|
154
|
+
checkpointCount: number;
|
|
155
|
+
tokensSaved: number;
|
|
156
|
+
compressedOriginalBytes: number;
|
|
157
|
+
lastCompactedAt?: number | null;
|
|
158
|
+
},
|
|
159
|
+
indexDir: string = getIndexDir(),
|
|
160
|
+
): void {
|
|
161
|
+
const db = openIndexStore(indexDir);
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
db.prepare(
|
|
164
|
+
`INSERT INTO repo_registry
|
|
165
|
+
(repo_root, display_name, state_dir, first_seen, last_seen, last_compacted_at,
|
|
166
|
+
checkpoint_count, tokens_saved, compressed_original_bytes)
|
|
167
|
+
VALUES (@repo_root, @display_name, @state_dir, @now, @now, @last_compacted_at,
|
|
168
|
+
@checkpoint_count, @tokens_saved, @compressed_original_bytes)
|
|
169
|
+
ON CONFLICT(repo_root) DO UPDATE SET
|
|
170
|
+
display_name = excluded.display_name,
|
|
171
|
+
state_dir = excluded.state_dir,
|
|
172
|
+
last_seen = excluded.last_seen,
|
|
173
|
+
last_compacted_at = COALESCE(excluded.last_compacted_at, repo_registry.last_compacted_at),
|
|
174
|
+
checkpoint_count = excluded.checkpoint_count,
|
|
175
|
+
tokens_saved = excluded.tokens_saved,
|
|
176
|
+
compressed_original_bytes = excluded.compressed_original_bytes`,
|
|
177
|
+
).run({
|
|
178
|
+
repo_root: row.repoRoot,
|
|
179
|
+
display_name: row.displayName,
|
|
180
|
+
state_dir: row.stateDir,
|
|
181
|
+
now,
|
|
182
|
+
last_compacted_at: row.lastCompactedAt ?? null,
|
|
183
|
+
checkpoint_count: row.checkpointCount,
|
|
184
|
+
tokens_saved: row.tokensSaved,
|
|
185
|
+
compressed_original_bytes: row.compressedOriginalBytes,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Record the active model/provider for a repo in the global index (denormalized
|
|
191
|
+
* so the All-repos table shows model without opening each repo's DB). Upserts a
|
|
192
|
+
* bare registry row if the repo isn't registered yet.
|
|
193
|
+
*/
|
|
194
|
+
export function recordRepoModel(
|
|
195
|
+
repoRoot: string,
|
|
196
|
+
model: {
|
|
197
|
+
provider: string;
|
|
198
|
+
providerName: string | null;
|
|
199
|
+
modelName: string | null;
|
|
200
|
+
inputRate: number;
|
|
201
|
+
outputRate: number;
|
|
202
|
+
stateDir: string;
|
|
203
|
+
displayName: string;
|
|
204
|
+
},
|
|
205
|
+
indexDir: string = getIndexDir(),
|
|
206
|
+
): void {
|
|
207
|
+
const db = openIndexStore(indexDir);
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
db.prepare(
|
|
210
|
+
`INSERT INTO repo_registry
|
|
211
|
+
(repo_root, display_name, state_dir, first_seen, last_seen,
|
|
212
|
+
provider, provider_name, model_name, input_rate, output_rate, model_captured_at)
|
|
213
|
+
VALUES (@repo_root, @display_name, @state_dir, @now, @now,
|
|
214
|
+
@provider, @provider_name, @model_name, @input_rate, @output_rate, @now)
|
|
215
|
+
ON CONFLICT(repo_root) DO UPDATE SET
|
|
216
|
+
last_seen = excluded.last_seen,
|
|
217
|
+
provider = excluded.provider,
|
|
218
|
+
provider_name = excluded.provider_name,
|
|
219
|
+
model_name = excluded.model_name,
|
|
220
|
+
input_rate = excluded.input_rate,
|
|
221
|
+
output_rate = excluded.output_rate,
|
|
222
|
+
model_captured_at = excluded.model_captured_at`,
|
|
223
|
+
).run({
|
|
224
|
+
repo_root: repoRoot,
|
|
225
|
+
display_name: model.displayName,
|
|
226
|
+
state_dir: model.stateDir,
|
|
227
|
+
now,
|
|
228
|
+
provider: model.provider,
|
|
229
|
+
provider_name: model.providerName,
|
|
230
|
+
model_name: model.modelName,
|
|
231
|
+
input_rate: model.inputRate,
|
|
232
|
+
output_rate: model.outputRate,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function mapRegistryRow(row: any): RepoRegistryRow {
|
|
237
|
+
return {
|
|
238
|
+
repoRoot: row.repo_root,
|
|
239
|
+
displayName: row.display_name ?? "",
|
|
240
|
+
stateDir: row.state_dir,
|
|
241
|
+
firstSeen: row.first_seen ?? 0,
|
|
242
|
+
lastSeen: row.last_seen ?? 0,
|
|
243
|
+
lastCompactedAt: row.last_compacted_at ?? null,
|
|
244
|
+
checkpointCount: row.checkpoint_count ?? 0,
|
|
245
|
+
tokensSaved: row.tokens_saved ?? 0,
|
|
246
|
+
compressedOriginalBytes: row.compressed_original_bytes ?? 0,
|
|
247
|
+
provider: row.provider ?? null,
|
|
248
|
+
providerName: row.provider_name ?? null,
|
|
249
|
+
modelName: row.model_name ?? null,
|
|
250
|
+
inputRate: row.input_rate ?? null,
|
|
251
|
+
outputRate: row.output_rate ?? null,
|
|
252
|
+
modelCapturedAt: row.model_captured_at ?? null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** All registered repos, most-recently-seen first. */
|
|
257
|
+
export function listRepoRegistry(indexDir: string = getIndexDir()): RepoRegistryRow[] {
|
|
258
|
+
const db = openIndexStore(indexDir);
|
|
259
|
+
const rows = db.prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC").all() as any[];
|
|
260
|
+
return rows.map(mapRegistryRow);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A single repo's registry row, or undefined. */
|
|
264
|
+
export function getRepoRegistry(repoRoot: string, indexDir: string = getIndexDir()): RepoRegistryRow | undefined {
|
|
265
|
+
const db = openIndexStore(indexDir);
|
|
266
|
+
const row = db.prepare("SELECT * FROM repo_registry WHERE repo_root = ?").get(repoRoot) as any;
|
|
267
|
+
return row ? mapRegistryRow(row) : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Close the cached index connection (test teardown only). */
|
|
271
|
+
export function closeIndexStore(): void {
|
|
272
|
+
if (indexCache) {
|
|
273
|
+
indexCache.close();
|
|
274
|
+
indexCache = undefined;
|
|
275
|
+
indexCacheDir = undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
65
279
|
function initSchema(db: Database.Database): void {
|
|
66
280
|
db.exec(`
|
|
67
281
|
CREATE TABLE IF NOT EXISTS context_chunks (
|