pi-mega-compact 0.4.17 → 0.4.19
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 +303 -1
- 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 +326 -1
- 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
|
// ---------------------------------------------------------------------------
|
|
@@ -34,6 +99,7 @@ function readSnapshot(snapshotPath) {
|
|
|
34
99
|
crew: { activeAgents: 0, currentTurn: 0 },
|
|
35
100
|
repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
|
|
36
101
|
integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
|
|
102
|
+
model: undefined,
|
|
37
103
|
};
|
|
38
104
|
}
|
|
39
105
|
}
|
|
@@ -111,14 +177,54 @@ function dashboardHtml(tierName) {
|
|
|
111
177
|
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
112
178
|
.empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
113
179
|
.offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
|
|
180
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
181
|
+
.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; }
|
|
182
|
+
.tab:hover { color: #c9d1d9; border-color: #484f58; }
|
|
183
|
+
.tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
|
|
184
|
+
.tab-panel { display: none; }
|
|
185
|
+
.tab-panel.active { display: block; }
|
|
186
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
187
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
188
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
189
|
+
.summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
190
|
+
table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
|
|
191
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
192
|
+
table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
|
|
193
|
+
table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
|
|
194
|
+
table.repos tr:last-child td { border-bottom: none; }
|
|
195
|
+
table.repos tr:hover td { background: #1c2128; }
|
|
196
|
+
.repo-model { color: #a371f7; }
|
|
197
|
+
.repo-none { color: #484f58; font-style: italic; }
|
|
198
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
199
|
+
.model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
|
|
200
|
+
.card.cost h2 { color: #a371f7; }
|
|
201
|
+
.cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
|
|
202
|
+
.cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
|
|
203
|
+
.repo-link { cursor: pointer; }
|
|
204
|
+
.repo-link:hover td { color: #58a6ff; }
|
|
205
|
+
.repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
206
|
+
.repo-detail.open { display: flex; }
|
|
207
|
+
.repo-detail-box { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
|
|
208
|
+
.repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
|
|
209
|
+
.repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
|
|
210
|
+
.repo-close:hover { color: #f0f6fc; }
|
|
211
|
+
.repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
|
|
114
212
|
</style>
|
|
115
213
|
</head>
|
|
116
214
|
<body>
|
|
117
215
|
|
|
118
216
|
<div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
|
|
119
217
|
|
|
120
|
-
<h1><span>mega-compact</span><span class="tier">${tierName}</span></h1>
|
|
218
|
+
<h1><span>mega-compact</span><span class="tier">${tierName}</span><span class="model-pill" id="hdr-model">—</span></h1>
|
|
121
219
|
|
|
220
|
+
<nav class="tabs">
|
|
221
|
+
<button class="tab active" data-tab="current">Current repo</button>
|
|
222
|
+
<button class="tab" data-tab="all">All repos</button>
|
|
223
|
+
<button class="tab" data-tab="summary">Summary</button>
|
|
224
|
+
</nav>
|
|
225
|
+
|
|
226
|
+
<!-- Current repo (existing single-repo view) -->
|
|
227
|
+
<div class="tab-panel" id="panel-current">
|
|
122
228
|
<div class="grid">
|
|
123
229
|
<div class="card">
|
|
124
230
|
<h2>Context Window</h2>
|
|
@@ -178,6 +284,17 @@ function dashboardHtml(tierName) {
|
|
|
178
284
|
<span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
|
|
179
285
|
</div>
|
|
180
286
|
</div>
|
|
287
|
+
<div class="card cost">
|
|
288
|
+
<h2>💰 Model & Cost Savings</h2>
|
|
289
|
+
<div class="cost-usd" id="cost-usd">≈ $0.00 saved</div>
|
|
290
|
+
<div class="cost-sub" id="cost-windows">0 context-windows extended</div>
|
|
291
|
+
<div class="stat-grid" style="margin-top:12px">
|
|
292
|
+
<span class="label" title="The model pi is currently using — its pricing drives the cost figure.">Model</span><span class="value" id="md-name">—</span>
|
|
293
|
+
<span class="label" title="The provider serving the model.">Provider</span><span class="value" id="md-provider">—</span>
|
|
294
|
+
<span class="label" title="USD per input token, from the model's pricing.">Input Rate</span><span class="value" id="md-input">—</span>
|
|
295
|
+
<span class="label" title="USD per output token, from the model's pricing.">Output Rate</span><span class="value" id="md-output">—</span>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
181
298
|
<div class="card">
|
|
182
299
|
<h2>Crew / Agents</h2>
|
|
183
300
|
<div class="stat-grid">
|
|
@@ -205,7 +322,67 @@ function dashboardHtml(tierName) {
|
|
|
205
322
|
<div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
|
|
206
323
|
</div>
|
|
207
324
|
|
|
325
|
+
<h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
|
|
326
|
+
<table class="repos">
|
|
327
|
+
<thead>
|
|
328
|
+
<tr>
|
|
329
|
+
<th>Repo</th><th>Model</th>
|
|
330
|
+
<th style="text-align:right">Checkpoints</th>
|
|
331
|
+
<th style="text-align:right">Tokens Saved</th>
|
|
332
|
+
<th style="text-align:right">Retained</th>
|
|
333
|
+
<th style="text-align:right">Last Compacted</th>
|
|
334
|
+
</tr>
|
|
335
|
+
</thead>
|
|
336
|
+
<tbody id="cur-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
|
|
337
|
+
</table>
|
|
338
|
+
<div class="updated" id="cur-updated"></div>
|
|
339
|
+
|
|
208
340
|
<div class="updated" id="updated"></div>
|
|
341
|
+
</div><!-- /panel-current -->
|
|
342
|
+
|
|
343
|
+
<!-- Per-repo detail modal -->
|
|
344
|
+
<div class="repo-detail" id="repo-detail">
|
|
345
|
+
<div class="repo-detail-box">
|
|
346
|
+
<h2><span id="rd-name">Repo</span><button class="repo-close" id="rd-close" title="Close">×</button></h2>
|
|
347
|
+
<div class="repo-path" id="rd-path"></div>
|
|
348
|
+
<div class="stat-grid">
|
|
349
|
+
<span class="label">Model</span><span class="value" id="rd-model">—</span>
|
|
350
|
+
<span class="label">Checkpoints</span><span class="value" id="rd-cp">0</span>
|
|
351
|
+
<span class="label">Tokens Saved</span><span class="value" id="rd-saved">0</span>
|
|
352
|
+
<span class="label">Compressed-Original</span><span class="value" id="rd-bytes">0 B</span>
|
|
353
|
+
<span class="label">Last Compacted</span><span class="value" id="rd-when">—</span>
|
|
354
|
+
<span class="label">Provider</span><span class="value" id="rd-provider">—</span>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<!-- All repos (machine-wide registry from index.sqlite) -->
|
|
360
|
+
<div class="tab-panel" id="panel-all">
|
|
361
|
+
<table class="repos">
|
|
362
|
+
<thead>
|
|
363
|
+
<tr>
|
|
364
|
+
<th>Repo</th><th>Model</th>
|
|
365
|
+
<th style="text-align:right">Checkpoints</th>
|
|
366
|
+
<th style="text-align:right">Tokens Saved</th>
|
|
367
|
+
<th style="text-align:right">Retained</th>
|
|
368
|
+
<th style="text-align:right">Last Compacted</th>
|
|
369
|
+
</tr>
|
|
370
|
+
</thead>
|
|
371
|
+
<tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
|
|
372
|
+
</table>
|
|
373
|
+
<div class="updated" id="all-updated"></div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<!-- Summary (aggregate across all repos) -->
|
|
377
|
+
<div class="tab-panel" id="panel-summary">
|
|
378
|
+
<div class="summary-grid">
|
|
379
|
+
<div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
|
|
380
|
+
<div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
|
|
381
|
+
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
382
|
+
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="updated" id="sm-updated"></div>
|
|
385
|
+
</div>
|
|
209
386
|
|
|
210
387
|
<script>
|
|
211
388
|
(function() {
|
|
@@ -285,6 +462,24 @@ function dashboardHtml(tierName) {
|
|
|
285
462
|
document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
|
|
286
463
|
document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
|
|
287
464
|
|
|
465
|
+
// --- Active model + cost savings (same calc as /mega-status) ---------------
|
|
466
|
+
var model = d.model;
|
|
467
|
+
document.getElementById('hdr-model').textContent = model && model.name ? model.name : '—';
|
|
468
|
+
document.getElementById('md-name').textContent = model && model.name ? model.name : '—';
|
|
469
|
+
document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
|
|
470
|
+
document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
|
|
471
|
+
document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
|
|
472
|
+
if (model && model.inputRate && repo.tokensSaved > 0) {
|
|
473
|
+
var usd = (repo.tokensSaved * model.inputRate);
|
|
474
|
+
var win = d.context.contextWindow || 0;
|
|
475
|
+
var windows = win > 0 ? (repo.tokensSaved / win).toFixed(1) : '0';
|
|
476
|
+
document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
|
|
477
|
+
document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
|
|
478
|
+
} else {
|
|
479
|
+
document.getElementById('cost-usd').textContent = '≈ $0.00 saved';
|
|
480
|
+
document.getElementById('cost-windows').textContent = '0 context-windows extended';
|
|
481
|
+
}
|
|
482
|
+
|
|
288
483
|
document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
|
|
289
484
|
}
|
|
290
485
|
|
|
@@ -331,6 +526,105 @@ function dashboardHtml(tierName) {
|
|
|
331
526
|
};
|
|
332
527
|
}
|
|
333
528
|
connectSSE();
|
|
529
|
+
|
|
530
|
+
// --- Multi-repo (index.sqlite via /api/index) ---------------------------
|
|
531
|
+
function fmtBytesTop(b) {
|
|
532
|
+
b = b || 0;
|
|
533
|
+
if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
|
|
534
|
+
if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
|
|
535
|
+
return b + ' B';
|
|
536
|
+
}
|
|
537
|
+
function renderIndex(d) {
|
|
538
|
+
d = d || { updatedAt: null, summary: null, repos: [] };
|
|
539
|
+
var repos = d.repos || [];
|
|
540
|
+
var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
|
|
541
|
+
document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
|
|
542
|
+
document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
|
|
543
|
+
document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
|
|
544
|
+
document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
|
|
545
|
+
|
|
546
|
+
// Shared clickable-row renderer for both the in-current table and the
|
|
547
|
+
// All-repos tab — each row opens the per-repo detail modal.
|
|
548
|
+
function rowsHtml() {
|
|
549
|
+
if (!repos.length) return '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
|
|
550
|
+
return repos.map(function(r) {
|
|
551
|
+
var model = r.modelName
|
|
552
|
+
? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
|
|
553
|
+
: '<span class="repo-none">—</span>';
|
|
554
|
+
var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
|
|
555
|
+
return '<tr class="repo-link" data-repo="' + sanitize(r.repoRoot) + '">' +
|
|
556
|
+
'<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
|
|
557
|
+
'<td>' + model + '</td>' +
|
|
558
|
+
'<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
|
|
559
|
+
'<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
|
|
560
|
+
'<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
|
|
561
|
+
'<td class="num">' + sanitize(when) + '</td>' +
|
|
562
|
+
'</tr>';
|
|
563
|
+
}).join('');
|
|
564
|
+
}
|
|
565
|
+
document.getElementById('cur-rows').innerHTML = rowsHtml();
|
|
566
|
+
document.getElementById('all-rows').innerHTML = rowsHtml();
|
|
567
|
+
bindRepoRows();
|
|
568
|
+
|
|
569
|
+
var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
|
|
570
|
+
document.getElementById('cur-updated').textContent = stamp;
|
|
571
|
+
document.getElementById('all-updated').textContent = stamp;
|
|
572
|
+
document.getElementById('sm-updated').textContent = stamp;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Per-repo detail modal ---------------------------------------------------
|
|
576
|
+
var detailEl = document.getElementById('repo-detail');
|
|
577
|
+
var indexCache = { repos: [] };
|
|
578
|
+
function openRepoDetail(root) {
|
|
579
|
+
var r = null;
|
|
580
|
+
for (var i = 0; i < indexCache.repos.length; i++) {
|
|
581
|
+
if (indexCache.repos[i].repoRoot === root) { r = indexCache.repos[i]; break; }
|
|
582
|
+
}
|
|
583
|
+
if (!r) return;
|
|
584
|
+
document.getElementById('rd-name').textContent = r.displayName || r.repoRoot;
|
|
585
|
+
document.getElementById('rd-path').textContent = r.repoRoot;
|
|
586
|
+
document.getElementById('rd-model').textContent = r.modelName || '—';
|
|
587
|
+
document.getElementById('rd-provider').textContent = r.providerName || (r.provider || '—');
|
|
588
|
+
document.getElementById('rd-cp').textContent = (r.checkpointCount || 0).toLocaleString();
|
|
589
|
+
document.getElementById('rd-saved').textContent = (r.tokensSaved || 0).toLocaleString();
|
|
590
|
+
document.getElementById('rd-bytes').textContent = fmtBytesTop(r.compressedOriginalBytes);
|
|
591
|
+
document.getElementById('rd-when').textContent = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
|
|
592
|
+
detailEl.classList.add('open');
|
|
593
|
+
}
|
|
594
|
+
document.getElementById('rd-close').addEventListener('click', function() { detailEl.classList.remove('open'); });
|
|
595
|
+
detailEl.addEventListener('click', function(e) { if (e.target === detailEl) detailEl.classList.remove('open'); });
|
|
596
|
+
function bindRepoRows() {
|
|
597
|
+
var rows = document.querySelectorAll('.repo-link');
|
|
598
|
+
for (var i = 0; i < rows.length; i++) {
|
|
599
|
+
rows[i].addEventListener('click', function() { openRepoDetail(this.getAttribute('data-repo')); });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function pollIndex() {
|
|
603
|
+
fetch('/api/index').then(function(r) { return r.json(); }).then(function(d) {
|
|
604
|
+
indexCache = d && d.repos ? d : indexCache;
|
|
605
|
+
renderIndex(d);
|
|
606
|
+
}).catch(function() {});
|
|
607
|
+
}
|
|
608
|
+
pollIndex();
|
|
609
|
+
setInterval(pollIndex, 5000);
|
|
610
|
+
|
|
611
|
+
// --- Tab switching ------------------------------------------------------
|
|
612
|
+
var tabs = document.querySelectorAll('.tab');
|
|
613
|
+
var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
|
|
614
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
615
|
+
tabs[i].addEventListener('click', function() {
|
|
616
|
+
var name = this.getAttribute('data-tab');
|
|
617
|
+
for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
|
|
618
|
+
this.classList.add('active');
|
|
619
|
+
for (var k in panels) {
|
|
620
|
+
if (Object.prototype.hasOwnProperty.call(panels, k)) {
|
|
621
|
+
var el = document.getElementById(panels[k]);
|
|
622
|
+
if (el) el.classList.toggle('active', k === name);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (name === 'all' || name === 'summary') pollIndex();
|
|
626
|
+
});
|
|
627
|
+
}
|
|
334
628
|
})();
|
|
335
629
|
</script>
|
|
336
630
|
</body>
|
|
@@ -380,6 +674,14 @@ export function launchDashboardServer(stateDir) {
|
|
|
380
674
|
res.end(JSON.stringify(snap));
|
|
381
675
|
return;
|
|
382
676
|
}
|
|
677
|
+
// Multi-repo aggregate (Phase 5b): the machine-wide repo registry read
|
|
678
|
+
// directly from SQLite (index.sqlite). Lets one dashboard show every repo's
|
|
679
|
+
// checkpoints, tokens saved, and active model. Read-only.
|
|
680
|
+
if (req.url === "/api/index") {
|
|
681
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
682
|
+
res.end(JSON.stringify(readIndex() ?? { updatedAt: null, summary: null, repos: [] }));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
383
685
|
if (req.url === "/api/events") {
|
|
384
686
|
res.writeHead(200, {
|
|
385
687
|
"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
|
|