pi-mega-compact 0.7.6 → 0.7.7
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/README.md +120 -31
- package/dist/extensions/dashboard-server.js +137 -4
- package/dist/extensions/dashboard-server.test.js +59 -0
- package/dist/extensions/mega-pipeline.js +25 -1
- package/dist/extensions/mega-runtime.js +31 -1
- package/dist/src/store/sqlite.cachehit.test.js +55 -0
- package/dist/src/store/sqlite.js +23 -0
- package/extensions/dashboard-server.test.ts +69 -0
- package/extensions/dashboard-server.ts +141 -1
- package/extensions/mega-dashboard.ts +17 -0
- package/extensions/mega-pipeline.ts +18 -1
- package/extensions/mega-runtime.ts +40 -1
- package/package.json +1 -1
- package/src/store/sqlite.cachehit.test.ts +68 -0
- package/src/store/sqlite.ts +28 -0
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite.cachehit.test.ts — tests for the live dashboard counters
|
|
3
|
+
* (incCompactCount / getCompactCount, incRecallInjected / getRecallInjected,
|
|
4
|
+
* incCacheHitTokens / getCacheHitTokensSaved). These reuse the schemaless
|
|
5
|
+
* `meta` integer counter, so no migration is required and the same on-disk
|
|
6
|
+
* SQLite store persists the tallies across (re)opens.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { openStore, incCompactCount, getCompactCount, incRecallInjected, getRecallInjected, incCacheHitTokens, getCacheHitTokensSaved, } from "./sqlite.js";
|
|
14
|
+
describe("live dashboard counters (meta integers)", () => {
|
|
15
|
+
let dir;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
dir = mkdtempSync(join(tmpdir(), "cachehit-test-"));
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
it("increments + reads compact_count and persists on reopen", () => {
|
|
23
|
+
incCompactCount(dir);
|
|
24
|
+
incCompactCount(dir);
|
|
25
|
+
assert.equal(getCompactCount(dir), 2);
|
|
26
|
+
// Ensure the store handle is (re)opened and the value is read back from disk.
|
|
27
|
+
openStore(dir);
|
|
28
|
+
assert.equal(getCompactCount(dir), 2);
|
|
29
|
+
});
|
|
30
|
+
it("accumulates recall injections + cache-hit tokens", () => {
|
|
31
|
+
incRecallInjected(3, dir);
|
|
32
|
+
incRecallInjected(2, dir);
|
|
33
|
+
assert.equal(getRecallInjected(dir), 5);
|
|
34
|
+
incCacheHitTokens(1200, dir);
|
|
35
|
+
incCacheHitTokens(800, dir);
|
|
36
|
+
assert.equal(getCacheHitTokensSaved(dir), 2000);
|
|
37
|
+
});
|
|
38
|
+
it("ignores non-positive increments (no-op)", () => {
|
|
39
|
+
incRecallInjected(0, dir);
|
|
40
|
+
incRecallInjected(-5, dir);
|
|
41
|
+
incCacheHitTokens(0, dir);
|
|
42
|
+
assert.equal(getRecallInjected(dir), 0);
|
|
43
|
+
assert.equal(getCacheHitTokensSaved(dir), 0);
|
|
44
|
+
});
|
|
45
|
+
it("persists all three counters across a fresh openStore handle", () => {
|
|
46
|
+
incCompactCount(dir);
|
|
47
|
+
incRecallInjected(4, dir);
|
|
48
|
+
incCacheHitTokens(500, dir);
|
|
49
|
+
// openStore returns the canonical (on-disk) handle; reading back proves durability.
|
|
50
|
+
openStore(dir);
|
|
51
|
+
assert.equal(getCompactCount(dir), 1);
|
|
52
|
+
assert.equal(getRecallInjected(dir), 4);
|
|
53
|
+
assert.equal(getCacheHitTokensSaved(dir), 500);
|
|
54
|
+
});
|
|
55
|
+
});
|
package/dist/src/store/sqlite.js
CHANGED
|
@@ -594,6 +594,29 @@ export function bumpDedupStats(deduped, stateDir = getStateDir()) {
|
|
|
594
594
|
if (deduped)
|
|
595
595
|
incMeta("deduped", 1, stateDir);
|
|
596
596
|
}
|
|
597
|
+
// --- Live dashboard counters (schemaless meta key/value — NO migration) -----
|
|
598
|
+
// These reuse the private `incMeta` atomically-incrementing integer counter so
|
|
599
|
+
// all cumulative tallies live in the same `meta` table as tokens_saved etc.
|
|
600
|
+
export function incCompactCount(stateDir = getStateDir()) {
|
|
601
|
+
incMeta("compact_count", 1, stateDir);
|
|
602
|
+
}
|
|
603
|
+
export function getCompactCount(stateDir = getStateDir()) {
|
|
604
|
+
return getMetaNumber("compact_count", stateDir);
|
|
605
|
+
}
|
|
606
|
+
export function incRecallInjected(n, stateDir = getStateDir()) {
|
|
607
|
+
if (n > 0)
|
|
608
|
+
incMeta("recall_injected", n, stateDir);
|
|
609
|
+
}
|
|
610
|
+
export function getRecallInjected(stateDir = getStateDir()) {
|
|
611
|
+
return getMetaNumber("recall_injected", stateDir);
|
|
612
|
+
}
|
|
613
|
+
export function incCacheHitTokens(delta, stateDir = getStateDir()) {
|
|
614
|
+
if (delta > 0)
|
|
615
|
+
incMeta("cache_hit_tokens_saved", delta, stateDir);
|
|
616
|
+
}
|
|
617
|
+
export function getCacheHitTokensSaved(stateDir = getStateDir()) {
|
|
618
|
+
return getMetaNumber("cache_hit_tokens_saved", stateDir);
|
|
619
|
+
}
|
|
597
620
|
// --- Future-feature foundation (resume sessions / daily log / lessons) -------
|
|
598
621
|
// Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
599
622
|
// day one. Full UI/recall for these lands in later sprints.
|
|
@@ -228,6 +228,75 @@ describe("multi-repo /api/index (S19)", () => {
|
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
describe("multi-repo /api/servers (active cache-hit stats)", () => {
|
|
232
|
+
test("returns active repos with live dashboard.json cache-hit/compaction stats", async () => {
|
|
233
|
+
const dir = mkdtempSync(join(tmpdir(), "dash-servers-"));
|
|
234
|
+
const indexDir = mkdtempSync(join(tmpdir(), "index-servers-"));
|
|
235
|
+
process.env.MEGACOMPACT_INDEX_DIR = indexDir;
|
|
236
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = "19323";
|
|
237
|
+
|
|
238
|
+
const { upsertRepoRegistry } = await import("../src/store/sqlite.js");
|
|
239
|
+
|
|
240
|
+
const activeState = mkdtempSync(join(tmpdir(), "srv-active-"));
|
|
241
|
+
writeFileSync(join(activeState, "dashboard.json"), JSON.stringify({
|
|
242
|
+
updatedAt: new Date().toISOString(),
|
|
243
|
+
tier: "high",
|
|
244
|
+
context: { tokens: 5000, percent: 0.42, contextWindow: 12000 },
|
|
245
|
+
session: { id: "s1", state: "idle" },
|
|
246
|
+
cacheHits: { session: 3, total: 7, sessionTokensSaved: 1200, totalTokensSaved: 9000 },
|
|
247
|
+
compacts: { session: 2, total: 5 },
|
|
248
|
+
timeSaved: { compact: { sessionSec: 1.5, totalSec: 4 }, cacheHit: { sessionSec: 0.6, totalSec: 4.5 } },
|
|
249
|
+
}, null, 2));
|
|
250
|
+
upsertRepoRegistry(
|
|
251
|
+
{ repoRoot: "/home/u/active", displayName: "active", stateDir: activeState, checkpointCount: 4, tokensSaved: 9000, compressedOriginalBytes: 0, lastSeen: Math.floor(Date.now() / 1000), modelName: "gpt-4o", providerName: "OpenAI" },
|
|
252
|
+
indexDir,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const staleState = mkdtempSync(join(tmpdir(), "srv-stale-"));
|
|
256
|
+
writeFileSync(join(staleState, "dashboard.json"), JSON.stringify({ updatedAt: new Date().toISOString(), tier: "low" }, null, 2));
|
|
257
|
+
const longAgo = Math.floor(Date.now() / 1000) - 7 * 86_400;
|
|
258
|
+
upsertRepoRegistry(
|
|
259
|
+
{ repoRoot: "/home/u/stale", displayName: "stale", stateDir: staleState, checkpointCount: 1, tokensSaved: 100, compressedOriginalBytes: 0, lastSeen: longAgo },
|
|
260
|
+
indexDir,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const child = spawn(process.execPath, [SERVER_ENTRY, dir], { stdio: "ignore" });
|
|
264
|
+
try {
|
|
265
|
+
await waitFor(async () => {
|
|
266
|
+
try {
|
|
267
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
268
|
+
const res = await fetch(`http://localhost:${raw.port}/api/version`);
|
|
269
|
+
return res.ok;
|
|
270
|
+
} catch { return false; }
|
|
271
|
+
});
|
|
272
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
273
|
+
const body = (await fetch(`http://localhost:${raw.port}/api/servers`).then((r) => r.json())) as {
|
|
274
|
+
updatedAt: string;
|
|
275
|
+
servers: Array<Record<string, unknown>>;
|
|
276
|
+
};
|
|
277
|
+
assert.equal(body.servers.length, 1, "only the active repo is returned");
|
|
278
|
+
const s = body.servers[0];
|
|
279
|
+
assert.equal(s.displayName, "active");
|
|
280
|
+
assert.equal(s.tier, "high");
|
|
281
|
+
assert.equal(s.model, "gpt-4o");
|
|
282
|
+
assert.equal(s.provider, "OpenAI");
|
|
283
|
+
assert.equal(s.contextPct, 0.42);
|
|
284
|
+
assert.equal(s.state, "idle");
|
|
285
|
+
assert.deepEqual(s.cacheHits, { session: 3, total: 7, sessionTokensSaved: 1200, totalTokensSaved: 9000 });
|
|
286
|
+
assert.deepEqual(s.compacts, { session: 2, total: 5 });
|
|
287
|
+
const ts = s.timeSaved as { compact: { sessionSec: number; totalSec: number }; cacheHit: { sessionSec: number; totalSec: number } };
|
|
288
|
+
assert.equal(ts.compact.sessionSec, 1.5);
|
|
289
|
+
assert.equal(ts.cacheHit.sessionSec, 0.6);
|
|
290
|
+
} finally {
|
|
291
|
+
child.kill("SIGTERM");
|
|
292
|
+
delete process.env.MEGACOMPACT_INDEX_DIR;
|
|
293
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
294
|
+
rmSync(dir, { recursive: true, force: true });
|
|
295
|
+
rmSync(indexDir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
231
300
|
// ---------------------------------------------------------------------------
|
|
232
301
|
// Lifecycle integration — launch the compiled server as a real subprocess
|
|
233
302
|
// (the same way the /dashboard command spawns it) and assert the two failure
|
|
@@ -46,6 +46,8 @@ function log(...parts: unknown[]): void {
|
|
|
46
46
|
// concurrent writer's WAL never blocks the request). All registry data lives in
|
|
47
47
|
// SQLite (the project's one-store invariant) — there is no JSON mirror. Same
|
|
48
48
|
// index-dir resolution as src/store/sqlite.ts getIndexDir().
|
|
49
|
+
const ACTIVE_WINDOW_SEC = 1800;
|
|
50
|
+
|
|
49
51
|
function getIndexDir(): string {
|
|
50
52
|
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
51
53
|
if (override && override.trim() !== "") return override;
|
|
@@ -266,6 +268,20 @@ interface Snapshot {
|
|
|
266
268
|
duplicatesCollapsed: number;
|
|
267
269
|
bytesPermanentlyDeleted: number;
|
|
268
270
|
};
|
|
271
|
+
cacheHits: {
|
|
272
|
+
session: number;
|
|
273
|
+
total: number;
|
|
274
|
+
sessionTokensSaved: number;
|
|
275
|
+
totalTokensSaved: number;
|
|
276
|
+
};
|
|
277
|
+
compacts: {
|
|
278
|
+
session: number;
|
|
279
|
+
total: number;
|
|
280
|
+
};
|
|
281
|
+
timeSaved: {
|
|
282
|
+
compact: { sessionSec: number; totalSec: number };
|
|
283
|
+
cacheHit: { sessionSec: number; totalSec: number };
|
|
284
|
+
};
|
|
269
285
|
compression: {
|
|
270
286
|
session: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
271
287
|
repo: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
@@ -283,6 +299,8 @@ interface Snapshot {
|
|
|
283
299
|
// Helpers
|
|
284
300
|
// ---------------------------------------------------------------------------
|
|
285
301
|
|
|
302
|
+
interface LiveSnapshot { tier?: string; updatedAt?: string | null; context?: { tokens?: number | null; percent?: number | null; contextWindow?: number }; session?: { id?: string; state?: string }; cacheHits?: { session: number; total: number; sessionTokensSaved: number; totalTokensSaved: number }; compacts?: { session: number; total: number }; timeSaved?: { compact: { sessionSec: number; totalSec: number }; cacheHit: { sessionSec: number; totalSec: number } }; }
|
|
303
|
+
|
|
286
304
|
function readSnapshot(snapshotPath: string) {
|
|
287
305
|
try {
|
|
288
306
|
const raw = readFileSync(snapshotPath, "utf-8");
|
|
@@ -302,6 +320,9 @@ function readSnapshot(snapshotPath: string) {
|
|
|
302
320
|
crew: { activeAgents: 0, currentTurn: 0 },
|
|
303
321
|
repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
|
|
304
322
|
integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
|
|
323
|
+
cacheHits: { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 },
|
|
324
|
+
compacts: { session: 0, total: 0 },
|
|
325
|
+
timeSaved: { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } },
|
|
305
326
|
compression: { session: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 }, repo: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 } },
|
|
306
327
|
model: undefined,
|
|
307
328
|
} as Snapshot;
|
|
@@ -426,6 +447,7 @@ function dashboardHtml(tierName: string): string {
|
|
|
426
447
|
<nav class="tabs">
|
|
427
448
|
<button class="tab active" data-tab="current">Current repo</button>
|
|
428
449
|
<button class="tab" data-tab="all">All repos</button>
|
|
450
|
+
<button class="tab" data-tab="active">Active Repos</button>
|
|
429
451
|
<button class="tab" data-tab="summary">Summary</button>
|
|
430
452
|
</nav>
|
|
431
453
|
|
|
@@ -527,6 +549,26 @@ function dashboardHtml(tierName: string): string {
|
|
|
527
549
|
</ul>
|
|
528
550
|
<p class="legend-note">Hover any label above for a quick explanation.</p>
|
|
529
551
|
</div>
|
|
552
|
+
<div class="card">
|
|
553
|
+
<h2>💾 Cache Hits & Compactions</h2>
|
|
554
|
+
<div class="stat-grid">
|
|
555
|
+
<span class="label">Cache Hits (session)</span><span class="value" id="ch-session">0</span>
|
|
556
|
+
<span class="label">Cache Hits (total)</span><span class="value" id="ch-total">0</span>
|
|
557
|
+
<span class="label">Tokens Saved (session)</span><span class="value" id="ch-tok-session">0</span>
|
|
558
|
+
<span class="label">Tokens Saved (total)</span><span class="value" id="ch-tok-total">0</span>
|
|
559
|
+
<span class="label">Compactions (session)</span><span class="value" id="cp-session">0</span>
|
|
560
|
+
<span class="label">Compactions (total)</span><span class="value" id="cp-total">0</span>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="card">
|
|
564
|
+
<h2>⏱ Time Saved (est.)</h2>
|
|
565
|
+
<div class="stat-grid">
|
|
566
|
+
<span class="label">Compact (session)</span><span class="value" id="ts-compact-session">0</span>
|
|
567
|
+
<span class="label">Compact (total)</span><span class="value" id="ts-compact-total">0</span>
|
|
568
|
+
<span class="label">Cache Hit (session)</span><span class="value" id="ts-cache-session">0</span>
|
|
569
|
+
<span class="label">Cache Hit (total)</span><span class="value" id="ts-cache-total">0</span>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
530
572
|
</div>
|
|
531
573
|
|
|
532
574
|
<div class="events">
|
|
@@ -552,6 +594,28 @@ function dashboardHtml(tierName: string): string {
|
|
|
552
594
|
<div class="updated" id="updated"></div>
|
|
553
595
|
</div><!-- /panel-current -->
|
|
554
596
|
|
|
597
|
+
<!-- Active repos (live cache-hit / compaction stats across machines) -->
|
|
598
|
+
<div class="tab-panel" id="panel-active">
|
|
599
|
+
<div class="card">
|
|
600
|
+
<h2>Active Repos — Live Cache Hits & Compactions</h2>
|
|
601
|
+
<p class="legend-note">Repos seen within the last 30 minutes, with their per-repo cache-hit, compaction, and time-saved (est.) totals pulled live from each repo's dashboard.json.</p>
|
|
602
|
+
<table class="repos">
|
|
603
|
+
<thead>
|
|
604
|
+
<tr>
|
|
605
|
+
<th>Repo</th><th>Model</th><th>Tier</th>
|
|
606
|
+
<th style="text-align:right">Context %</th><th>State</th>
|
|
607
|
+
<th style="text-align:right">Compactions (s/t)</th>
|
|
608
|
+
<th style="text-align:right">Cache Hits (s/t)</th>
|
|
609
|
+
<th style="text-align:right">Compact s/t (s)</th>
|
|
610
|
+
<th style="text-align:right">CacheHit s/t (s)</th>
|
|
611
|
+
</tr>
|
|
612
|
+
</thead>
|
|
613
|
+
<tbody id="active-rows"><tr><td colspan="9" class="repo-none">loading…</td></tr></tbody>
|
|
614
|
+
</table>
|
|
615
|
+
<div class="updated" id="active-updated"></div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
555
619
|
<!-- Per-repo detail modal -->
|
|
556
620
|
<div class="repo-detail" id="repo-detail">
|
|
557
621
|
<div class="repo-detail-box">
|
|
@@ -746,6 +810,21 @@ function dashboardHtml(tierName: string): string {
|
|
|
746
810
|
document.getElementById('cost-windows').textContent = '0 context-windows extended';
|
|
747
811
|
}
|
|
748
812
|
|
|
813
|
+
// --- Cache hits & compactions (live counters) ---------------------------
|
|
814
|
+
var ch = d.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
|
|
815
|
+
var cp = d.compacts || { session: 0, total: 0 };
|
|
816
|
+
var ts = d.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
|
|
817
|
+
document.getElementById('ch-session').textContent = (ch.session || 0).toLocaleString();
|
|
818
|
+
document.getElementById('ch-total').textContent = (ch.total || 0).toLocaleString();
|
|
819
|
+
document.getElementById('ch-tok-session').textContent = (ch.sessionTokensSaved || 0).toLocaleString();
|
|
820
|
+
document.getElementById('ch-tok-total').textContent = (ch.totalTokensSaved || 0).toLocaleString();
|
|
821
|
+
document.getElementById('cp-session').textContent = (cp.session || 0).toLocaleString();
|
|
822
|
+
document.getElementById('cp-total').textContent = (cp.total || 0).toLocaleString();
|
|
823
|
+
document.getElementById('ts-compact-session').textContent = fmtSec(ts.compact.sessionSec);
|
|
824
|
+
document.getElementById('ts-compact-total').textContent = fmtSec(ts.compact.totalSec);
|
|
825
|
+
document.getElementById('ts-cache-session').textContent = fmtSec(ts.cacheHit.sessionSec);
|
|
826
|
+
document.getElementById('ts-cache-total').textContent = fmtSec(ts.cacheHit.totalSec);
|
|
827
|
+
|
|
749
828
|
document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
|
|
750
829
|
}
|
|
751
830
|
|
|
@@ -951,9 +1030,51 @@ function dashboardHtml(tierName: string): string {
|
|
|
951
1030
|
pollIndex();
|
|
952
1031
|
setInterval(pollIndex, 5000);
|
|
953
1032
|
|
|
1033
|
+
// --- Active repos (live cache-hit / compaction stats) ---------------------
|
|
1034
|
+
function fmtSec(s) {
|
|
1035
|
+
s = s || 0;
|
|
1036
|
+
if (s >= 3600) return (s / 3600).toFixed(1) + 'h';
|
|
1037
|
+
if (s >= 60) return Math.round(s / 60) + 'm';
|
|
1038
|
+
if (s >= 1) return s.toFixed(1) + 's';
|
|
1039
|
+
return Math.round(s * 1000) + 'ms';
|
|
1040
|
+
}
|
|
1041
|
+
function renderActiveRepos(d) {
|
|
1042
|
+
d = d || { updatedAt: null, servers: [] };
|
|
1043
|
+
var servers = d.servers || [];
|
|
1044
|
+
var rowsEl = document.getElementById('active-rows');
|
|
1045
|
+
if (!rowsEl) return;
|
|
1046
|
+
if (!servers.length) {
|
|
1047
|
+
rowsEl.innerHTML = '<tr><td colspan="9" class="repo-none">No active repositories.</td></tr>';
|
|
1048
|
+
} else {
|
|
1049
|
+
rowsEl.innerHTML = servers.map(function(r) {
|
|
1050
|
+
var ch = r.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
|
|
1051
|
+
var cp = r.compacts || { session: 0, total: 0 };
|
|
1052
|
+
var ts = r.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
|
|
1053
|
+
return '<tr>' +
|
|
1054
|
+
'<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
|
|
1055
|
+
'<td>' + sanitize(r.model || '—') + '</td>' +
|
|
1056
|
+
'<td>' + sanitize(r.tier || '—') + '</td>' +
|
|
1057
|
+
'<td class="num">' + (r.contextPct != null ? Math.round(r.contextPct * 100) + '%' : '—') + '</td>' +
|
|
1058
|
+
'<td>' + sanitize(r.state || '—') + '</td>' +
|
|
1059
|
+
'<td class="num">' + (cp.session || 0) + ' / ' + (cp.total || 0) + '</td>' +
|
|
1060
|
+
'<td class="num">' + (ch.session || 0) + ' / ' + (ch.total || 0) + '</td>' +
|
|
1061
|
+
'<td class="num">' + fmtSec(ts.compact.sessionSec) + ' / ' + fmtSec(ts.compact.totalSec) + '</td>' +
|
|
1062
|
+
'<td class="num">' + fmtSec(ts.cacheHit.sessionSec) + ' / ' + fmtSec(ts.cacheHit.totalSec) + '</td>' +
|
|
1063
|
+
'</tr>';
|
|
1064
|
+
}).join('');
|
|
1065
|
+
}
|
|
1066
|
+
var upd = document.getElementById('active-updated');
|
|
1067
|
+
if (upd) upd.textContent = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
|
|
1068
|
+
}
|
|
1069
|
+
function pollServers() {
|
|
1070
|
+
fetch('/api/servers').then(function(r) { return r.json(); }).then(renderActiveRepos).catch(function() {});
|
|
1071
|
+
}
|
|
1072
|
+
pollServers();
|
|
1073
|
+
setInterval(pollServers, 5000);
|
|
1074
|
+
|
|
954
1075
|
// --- Tab switching ------------------------------------------------------
|
|
955
1076
|
var tabs = document.querySelectorAll('.tab');
|
|
956
|
-
var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
|
|
1077
|
+
var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary' };
|
|
957
1078
|
for (var i = 0; i < tabs.length; i++) {
|
|
958
1079
|
tabs[i].addEventListener('click', function() {
|
|
959
1080
|
var name = this.getAttribute('data-tab');
|
|
@@ -966,6 +1087,7 @@ function dashboardHtml(tierName: string): string {
|
|
|
966
1087
|
}
|
|
967
1088
|
}
|
|
968
1089
|
if (name === 'all' || name === 'summary') pollIndex();
|
|
1090
|
+
if (name === 'active') pollServers();
|
|
969
1091
|
});
|
|
970
1092
|
}
|
|
971
1093
|
})();
|
|
@@ -1170,6 +1292,24 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
1170
1292
|
return;
|
|
1171
1293
|
}
|
|
1172
1294
|
|
|
1295
|
+
if (req.url === "/api/servers") {
|
|
1296
|
+
try {
|
|
1297
|
+
const idx = readIndex();
|
|
1298
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
1299
|
+
const servers = (idx?.repos ?? []).filter((r) => (r.lastSeen ?? 0) >= nowSec - ACTIVE_WINDOW_SEC).map((r) => {
|
|
1300
|
+
const out: Record<string, unknown> = { repoRoot: r.repoRoot, displayName: r.displayName, model: r.modelName, provider: r.providerName, lastSeen: r.lastSeen, lastCompactedAt: r.lastCompactedAt };
|
|
1301
|
+
try { const p = join(r.stateDir, "dashboard.json"); if (existsSync(p)) { const snap = JSON.parse(readFileSync(p, "utf-8")) as LiveSnapshot; out.tier = snap.tier ?? null; out.contextPct = (snap.context && snap.context.percent != null) ? snap.context.percent : null; out.state = (snap.session && snap.session.state) || null; out.cacheHits = snap.cacheHits ?? null; out.compacts = snap.compacts ?? null; out.timeSaved = snap.timeSaved ?? null; out.updatedAt = snap.updatedAt ?? null; } } catch { /* best-effort */ }
|
|
1302
|
+
return out;
|
|
1303
|
+
}).sort((a, b) => (b.lastSeen as number) - (a.lastSeen as number));
|
|
1304
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1305
|
+
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), servers }));
|
|
1306
|
+
} catch {
|
|
1307
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1308
|
+
res.end(JSON.stringify({ error: "servers_unavailable" }));
|
|
1309
|
+
}
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1173
1313
|
if (req.url === "/api/events") {
|
|
1174
1314
|
res.writeHead(200, {
|
|
1175
1315
|
"Content-Type": "text/event-stream",
|
|
@@ -114,6 +114,23 @@ export interface DashboardSnapshot {
|
|
|
114
114
|
duplicatesCollapsed: number; // dedup duplicates (original kept on survivor)
|
|
115
115
|
bytesPermanentlyDeleted: number; // ALWAYS 0 — the invariant
|
|
116
116
|
};
|
|
117
|
+
/** Cache-hit / recall-injection counters (live session + store-wide totals). */
|
|
118
|
+
cacheHits: {
|
|
119
|
+
session: number; // dedup skips + recall injections this session
|
|
120
|
+
total: number; // store-wide deduped collapses + recall injections
|
|
121
|
+
sessionTokensSaved: number; // tokens saved via cache hits this session
|
|
122
|
+
totalTokensSaved: number; // store-wide tokens saved via cache hits
|
|
123
|
+
};
|
|
124
|
+
/** Compaction counters (live session + store-wide cumulative). */
|
|
125
|
+
compacts: {
|
|
126
|
+
session: number; // compactions performed this session
|
|
127
|
+
total: number; // store-wide cumulative compaction count
|
|
128
|
+
};
|
|
129
|
+
/** Estimated wall-clock time saved (rough tokens/sec heuristic). */
|
|
130
|
+
timeSaved: {
|
|
131
|
+
compact: { sessionSec: number; totalSec: number };
|
|
132
|
+
cacheHit: { sessionSec: number; totalSec: number };
|
|
133
|
+
};
|
|
117
134
|
/** Active model/provider (captured live) — shown on the current-repo card. */
|
|
118
135
|
model?: {
|
|
119
136
|
name: string; // Model.name or Model.id
|
|
@@ -15,7 +15,7 @@ import type { EngineMessage } from "../src/types.js";
|
|
|
15
15
|
import { recallAndInline, recallAndInlineAsync, formatRecallBlock, type RecallInjectResult } from "../src/recall.js";
|
|
16
16
|
import { normalizeSessionId } from "../src/store.js";
|
|
17
17
|
import { estimateBlockTokens } from "../src/tokens.js";
|
|
18
|
-
import { touchSession, logDaily } from "../src/store/sqlite.js";
|
|
18
|
+
import { touchSession, logDaily, incCompactCount, incRecallInjected, incCacheHitTokens } from "../src/store/sqlite.js";
|
|
19
19
|
import { consolidateMemories } from "../src/memory.js";
|
|
20
20
|
import {
|
|
21
21
|
type MegaRuntime,
|
|
@@ -146,6 +146,9 @@ function doCompact(
|
|
|
146
146
|
? result.originalTokenEstimate
|
|
147
147
|
: Math.max(0, result.originalTokenEstimate - result.tokenEstimate);
|
|
148
148
|
runtime.rt.tokensSaved += saved;
|
|
149
|
+
runtime.rt.compactCount += 1;
|
|
150
|
+
incCompactCount(runtime.currentStateDir);
|
|
151
|
+
if (result.deduped) { runtime.rt.cacheHitTokens += saved; incCacheHitTokens(saved, runtime.currentStateDir); }
|
|
149
152
|
runtime.rt.lastCompactAt = Date.now();
|
|
150
153
|
if (result.deduped) runtime.rt.dedupSkips++;
|
|
151
154
|
// Grow the rolling "saved" goal so the progress bar always has a fresh
|
|
@@ -435,6 +438,13 @@ export function doRecall(
|
|
|
435
438
|
runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
|
|
436
439
|
runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
|
|
437
440
|
}
|
|
441
|
+
if (result.toInject.length > 0) {
|
|
442
|
+
let sumTokens = 0; for (const h of result.toInject) sumTokens += h.checkpoint.tokenEstimate;
|
|
443
|
+
runtime.rt.recallInjections += result.toInject.length;
|
|
444
|
+
runtime.rt.cacheHitTokens += sumTokens;
|
|
445
|
+
incRecallInjected(result.toInject.length, runtime.currentStateDir);
|
|
446
|
+
incCacheHitTokens(sumTokens, runtime.currentStateDir);
|
|
447
|
+
}
|
|
438
448
|
return result;
|
|
439
449
|
}
|
|
440
450
|
|
|
@@ -493,6 +503,13 @@ export async function doRecallAsync(
|
|
|
493
503
|
if (!seen.has(h.checkpoint.checkpointId)) { merged.push(h); seen.add(h.checkpoint.checkpointId); }
|
|
494
504
|
}
|
|
495
505
|
const block = merged.length ? formatRecallBlock(merged) : "";
|
|
506
|
+
if (merged.length > 0) {
|
|
507
|
+
let sumTokens = 0; for (const h of merged) sumTokens += h.checkpoint.tokenEstimate;
|
|
508
|
+
runtime.rt.recallInjections += merged.length;
|
|
509
|
+
runtime.rt.cacheHitTokens += sumTokens;
|
|
510
|
+
incRecallInjected(merged.length, runtime.currentStateDir);
|
|
511
|
+
incCacheHitTokens(sumTokens, runtime.currentStateDir);
|
|
512
|
+
}
|
|
496
513
|
return {
|
|
497
514
|
toInject: merged,
|
|
498
515
|
report: merged.map((h) => ` • ${h.checkpoint.checkpointId}${h.repoId ? ` (from ${h.repoId.split("/").filter(Boolean).pop()})` : ""}`),
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
latestModelSnapshot,
|
|
25
25
|
upsertRepoRegistry,
|
|
26
26
|
recordRepoModel,
|
|
27
|
+
getDedupStats,
|
|
28
|
+
getCompactCount,
|
|
29
|
+
getRecallInjected,
|
|
30
|
+
getCacheHitTokensSaved,
|
|
27
31
|
type ModelSnapshot,
|
|
28
32
|
} from "../src/store/sqlite.js";
|
|
29
33
|
import { detectCrossRepoDrift } from "../src/driftDetection.js";
|
|
@@ -73,6 +77,10 @@ interface SessionRuntime {
|
|
|
73
77
|
tokensSaved: number; // this session-instance only: reset on session_start
|
|
74
78
|
lastCompactAt: number | null; // wall-clock ms of the last compaction this session
|
|
75
79
|
lastNativeCompactAt: number | null; // COMPACT-DEDUP FIX: wall-clock ms of the last NATIVE pi compaction (session_compact event) — used by the agent_end/legacy race guard to skip a redundant ctx.compact() that would throw "Already compacted".
|
|
80
|
+
// S25: live dashboard counters (reset on session_start, mirrored to SQLite).
|
|
81
|
+
compactCount: number; // compactions performed this session-instance
|
|
82
|
+
recallInjections: number; // recall blocks injected this session-instance
|
|
83
|
+
cacheHitTokens: number; // tokens saved via cache hits (dedup + recall) this session
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/** ANSI palette for the toolbar. The pi TUI's Text component preserves ANSI
|
|
@@ -94,6 +102,11 @@ export const C = {
|
|
|
94
102
|
|
|
95
103
|
const PULSE = ["◐", "◓", "◑", "◒"];
|
|
96
104
|
|
|
105
|
+
// Rough tokens-processed-per-second heuristic for the dashboard's "time saved"
|
|
106
|
+
// estimate. Throughput varies by model/hardware; this is order-of-magnitude so
|
|
107
|
+
// the dashboard can show a human-readable figure, not a precise measurement.
|
|
108
|
+
const TOKENS_PER_SEC_ESTIMATE = 2000;
|
|
109
|
+
|
|
97
110
|
// ── Full-width widget panel helpers ────────────────────────────────────────
|
|
98
111
|
// pi's above-editor widget renderer (a Container of Text lines) does NOT pass
|
|
99
112
|
// a terminal width to setWidget(), so lines render left-aligned by default. To
|
|
@@ -274,6 +287,9 @@ export class MegaRuntime {
|
|
|
274
287
|
tokensSaved: 0,
|
|
275
288
|
lastCompactAt: null,
|
|
276
289
|
lastNativeCompactAt: null,
|
|
290
|
+
compactCount: 0,
|
|
291
|
+
recallInjections: 0,
|
|
292
|
+
cacheHitTokens: 0,
|
|
277
293
|
};
|
|
278
294
|
debounceUntil = 0;
|
|
279
295
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -485,6 +501,12 @@ export class MegaRuntime {
|
|
|
485
501
|
const st = this.store.stats(this.rt.sessionId);
|
|
486
502
|
const repo = this.store.repoStats();
|
|
487
503
|
const di = this.store.dataInvariant();
|
|
504
|
+
// Live + store-wide cache-hit / compaction counters for the dashboard.
|
|
505
|
+
const ds = getDedupStats(this.currentStateDir);
|
|
506
|
+
const cacheHitsTotal = ds.deduped + getRecallInjected(this.currentStateDir);
|
|
507
|
+
const cacheHitsTotalTokens = getCacheHitTokensSaved(this.currentStateDir);
|
|
508
|
+
const cacheHitsSession = this.rt.dedupSkips + this.rt.recallInjections;
|
|
509
|
+
const sec = (tok: number) => (tok || 0) / TOKENS_PER_SEC_ESTIMATE;
|
|
488
510
|
// Active model/provider for the current-repo card + the multi-repo table.
|
|
489
511
|
const modelSnap = latestModelSnapshot(this.currentStateDir);
|
|
490
512
|
const model = modelSnap
|
|
@@ -604,6 +626,20 @@ export class MegaRuntime {
|
|
|
604
626
|
duplicatesCollapsed: di.duplicatesCollapsed,
|
|
605
627
|
bytesPermanentlyDeleted: di.bytesPermanentlyDeleted,
|
|
606
628
|
},
|
|
629
|
+
cacheHits: {
|
|
630
|
+
session: cacheHitsSession,
|
|
631
|
+
total: cacheHitsTotal,
|
|
632
|
+
sessionTokensSaved: this.rt.cacheHitTokens,
|
|
633
|
+
totalTokensSaved: cacheHitsTotalTokens,
|
|
634
|
+
},
|
|
635
|
+
compacts: {
|
|
636
|
+
session: this.rt.compactCount,
|
|
637
|
+
total: getCompactCount(this.currentStateDir),
|
|
638
|
+
},
|
|
639
|
+
timeSaved: {
|
|
640
|
+
compact: { sessionSec: sec(this.rt.tokensSaved), totalSec: sec(this.store.repoStats().tokensSaved) },
|
|
641
|
+
cacheHit: { sessionSec: sec(this.rt.cacheHitTokens), totalSec: sec(cacheHitsTotalTokens) },
|
|
642
|
+
},
|
|
607
643
|
model,
|
|
608
644
|
} as DashboardSnapshot);
|
|
609
645
|
|
|
@@ -849,7 +885,10 @@ export class MegaRuntime {
|
|
|
849
885
|
tokensSaved: 0,
|
|
850
886
|
lastCompactAt: null,
|
|
851
887
|
lastNativeCompactAt: null,
|
|
852
|
-
|
|
888
|
+
compactCount: 0,
|
|
889
|
+
recallInjections: 0,
|
|
890
|
+
cacheHitTokens: 0,
|
|
891
|
+
};
|
|
853
892
|
this.statusKey = undefined;
|
|
854
893
|
this.activeAgents = 0;
|
|
855
894
|
this.currentTurn = 0;
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite.cachehit.test.ts — tests for the live dashboard counters
|
|
3
|
+
* (incCompactCount / getCompactCount, incRecallInjected / getRecallInjected,
|
|
4
|
+
* incCacheHitTokens / getCacheHitTokensSaved). These reuse the schemaless
|
|
5
|
+
* `meta` integer counter, so no migration is required and the same on-disk
|
|
6
|
+
* SQLite store persists the tallies across (re)opens.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
openStore,
|
|
15
|
+
incCompactCount,
|
|
16
|
+
getCompactCount,
|
|
17
|
+
incRecallInjected,
|
|
18
|
+
getRecallInjected,
|
|
19
|
+
incCacheHitTokens,
|
|
20
|
+
getCacheHitTokensSaved,
|
|
21
|
+
} from "./sqlite.js";
|
|
22
|
+
|
|
23
|
+
describe("live dashboard counters (meta integers)", () => {
|
|
24
|
+
let dir: string;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
dir = mkdtempSync(join(tmpdir(), "cachehit-test-"));
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("increments + reads compact_count and persists on reopen", () => {
|
|
33
|
+
incCompactCount(dir);
|
|
34
|
+
incCompactCount(dir);
|
|
35
|
+
assert.equal(getCompactCount(dir), 2);
|
|
36
|
+
// Ensure the store handle is (re)opened and the value is read back from disk.
|
|
37
|
+
openStore(dir);
|
|
38
|
+
assert.equal(getCompactCount(dir), 2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("accumulates recall injections + cache-hit tokens", () => {
|
|
42
|
+
incRecallInjected(3, dir);
|
|
43
|
+
incRecallInjected(2, dir);
|
|
44
|
+
assert.equal(getRecallInjected(dir), 5);
|
|
45
|
+
incCacheHitTokens(1200, dir);
|
|
46
|
+
incCacheHitTokens(800, dir);
|
|
47
|
+
assert.equal(getCacheHitTokensSaved(dir), 2000);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("ignores non-positive increments (no-op)", () => {
|
|
51
|
+
incRecallInjected(0, dir);
|
|
52
|
+
incRecallInjected(-5, dir);
|
|
53
|
+
incCacheHitTokens(0, dir);
|
|
54
|
+
assert.equal(getRecallInjected(dir), 0);
|
|
55
|
+
assert.equal(getCacheHitTokensSaved(dir), 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("persists all three counters across a fresh openStore handle", () => {
|
|
59
|
+
incCompactCount(dir);
|
|
60
|
+
incRecallInjected(4, dir);
|
|
61
|
+
incCacheHitTokens(500, dir);
|
|
62
|
+
// openStore returns the canonical (on-disk) handle; reading back proves durability.
|
|
63
|
+
openStore(dir);
|
|
64
|
+
assert.equal(getCompactCount(dir), 1);
|
|
65
|
+
assert.equal(getRecallInjected(dir), 4);
|
|
66
|
+
assert.equal(getCacheHitTokensSaved(dir), 500);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/store/sqlite.ts
CHANGED
|
@@ -704,6 +704,34 @@ export function bumpDedupStats(deduped: boolean, stateDir: string = getStateDir(
|
|
|
704
704
|
if (deduped) incMeta("deduped", 1, stateDir);
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
+
// --- Live dashboard counters (schemaless meta key/value — NO migration) -----
|
|
708
|
+
// These reuse the private `incMeta` atomically-incrementing integer counter so
|
|
709
|
+
// all cumulative tallies live in the same `meta` table as tokens_saved etc.
|
|
710
|
+
|
|
711
|
+
export function incCompactCount(stateDir: string = getStateDir()): void {
|
|
712
|
+
incMeta("compact_count", 1, stateDir);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function getCompactCount(stateDir: string = getStateDir()): number {
|
|
716
|
+
return getMetaNumber("compact_count", stateDir);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function incRecallInjected(n: number, stateDir: string = getStateDir()): void {
|
|
720
|
+
if (n > 0) incMeta("recall_injected", n, stateDir);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function getRecallInjected(stateDir: string = getStateDir()): number {
|
|
724
|
+
return getMetaNumber("recall_injected", stateDir);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function incCacheHitTokens(delta: number, stateDir: string = getStateDir()): void {
|
|
728
|
+
if (delta > 0) incMeta("cache_hit_tokens_saved", delta, stateDir);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function getCacheHitTokensSaved(stateDir: string = getStateDir()): number {
|
|
732
|
+
return getMetaNumber("cache_hit_tokens_saved", stateDir);
|
|
733
|
+
}
|
|
734
|
+
|
|
707
735
|
// --- Future-feature foundation (resume sessions / daily log / lessons) -------
|
|
708
736
|
// Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
709
737
|
// day one. Full UI/recall for these lands in later sprints.
|