pi-mega-compact 0.7.6 → 0.7.8
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-compact.test.js +221 -22
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-events.js +64 -17
- package/dist/extensions/mega-pipeline.js +25 -1
- package/dist/extensions/mega-runtime.js +50 -5
- 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-compact.test.ts +296 -35
- package/extensions/mega-config.ts +22 -0
- package/extensions/mega-dashboard.ts +17 -0
- package/extensions/mega-events.ts +66 -18
- package/extensions/mega-pipeline.ts +18 -1
- package/extensions/mega-runtime.ts +61 -5
- 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
package/README.md
CHANGED
|
@@ -6,22 +6,37 @@ sessions into a **local SQLite store** and offers **deduped inline recall** —
|
|
|
6
6
|
running **locally inside the extension**, with **no remote MCP server** and
|
|
7
7
|
**zero network calls at runtime** (PREVENT-PI-004).
|
|
8
8
|
|
|
9
|
-
> **
|
|
10
|
-
> (`DatabaseSync`,
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
> mirror + dedup pipeline** (byte-stable prompt cache via deterministic epoch
|
|
14
|
-
> nonce) and **DB maintenance /commands** (`/mega-db-stats` · `prune` ·
|
|
15
|
-
> `vacuum` · `check` · `reconcile`) plus best-effort auto-maintenance on
|
|
16
|
-
> `session_start`. Legacy `.checkpoints.json.gz` snapshots are retained as
|
|
17
|
-
> disaster-recovery fallbacks and auto-imported on first run. The S24 line ties
|
|
18
|
-
> auto-compact, the tier label, trim depth, and durable-memory review to one
|
|
19
|
-
> **unified pressure signal**, adds a **cross-repo memory-RAG index**, and
|
|
20
|
-
> relieves context **during team runs** (not just at the end).
|
|
9
|
+
> **Status - v0.7.7.** Storage uses the built-in `node:sqlite` backend
|
|
10
|
+
> (`DatabaseSync`, Node >=22.13): zero native build step, fully local, and zero
|
|
11
|
+
> network at runtime. See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full
|
|
12
|
+
> changelog.
|
|
21
13
|
|
|
22
14
|
---
|
|
23
15
|
|
|
24
|
-
##
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Local & private** - everything stays on your disk. No telemetry, no API key, no MCP server, no cloud. The only network surface is an optional localhost dashboard you open yourself.
|
|
19
|
+
- **Two-layer compaction** - a non-destructive live summary every LLM call, plus durable checkpoints that relieve context mid-run (not just at the end).
|
|
20
|
+
- **Vector store + dedup** - each checkpoint is embedded and stored locally; an L0->L2 + RAPTOR cascade collapses duplicate work so storage and recall stay lean.
|
|
21
|
+
- **Automatic recall** - the most relevant checkpoints are re-inlined on resume or branch switch; cross-repo memory-RAG augments a thin store with decisions from other repos.
|
|
22
|
+
- **Live dashboard** - a localhost-only view of token usage, store stats, savings, per-repo activity, and an **Active Repos tab** that shows every currently-open session (last 30 minutes) side by side — so running multiple sessions at once is visible in one place.
|
|
23
|
+
- **Database maintenance** - `/mega-db-*` commands plus best-effort auto-maintenance on session start.
|
|
24
|
+
|
|
25
|
+
## Table of contents
|
|
26
|
+
|
|
27
|
+
- [Overview](#overview)
|
|
28
|
+
- [How it works](#how-it-works)
|
|
29
|
+
- [Installation](#installation)
|
|
30
|
+
- [Usage](#usage)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Dashboard](#dashboard)
|
|
33
|
+
- [Architecture](#architecture)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
- [Testing & bug reports](#testing--bug-reports)
|
|
36
|
+
- [Acknowledgements](#acknowledgements)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
25
40
|
|
|
26
41
|
pi's context window is finite. When a session gets long — especially a team run
|
|
27
42
|
with sub-agents — pi-mega-compact keeps it going without overflowing:
|
|
@@ -217,7 +232,7 @@ rm -f ~/.pi/agent/extensions/pi-mega-compact
|
|
|
217
232
|
|
|
218
233
|
---
|
|
219
234
|
|
|
220
|
-
##
|
|
235
|
+
## Usage
|
|
221
236
|
|
|
222
237
|
Once installed and registered, pi-mega-compact runs **automatically** — you don't
|
|
223
238
|
have to drive it. Past the context threshold it compacts in the background and
|
|
@@ -225,6 +240,8 @@ drops a checkpoint; on resume it re-inlines the relevant ones silently.
|
|
|
225
240
|
|
|
226
241
|
The commands (slash commands inside pi):
|
|
227
242
|
|
|
243
|
+
### Commands
|
|
244
|
+
|
|
228
245
|
| Command | Description |
|
|
229
246
|
|---|---|
|
|
230
247
|
| `/mega-compact [summary...]` | Manually compact the current session. A summary arg is used verbatim; otherwise the COLLAPSE heuristics build one. Persists a `chkpt_xxx`. |
|
|
@@ -242,18 +259,21 @@ The commands (slash commands inside pi):
|
|
|
242
259
|
| `/mega-db-vacuum` | `VACUUM` the DB (rebuild pages, reclaim freelist). Heavy: briefly doubles disk usage. |
|
|
243
260
|
| `/mega-db-check` | `PRAGMA integrity_check` + `wal_checkpoint(TRUNCATE)`. Fold the WAL into the main file and verify DB health. Use after a crash. |
|
|
244
261
|
| `/mega-db-reconcile` | Fix `dedup_mirror.ref_count` drift vs actual `raw_transcript` refs, delete orphan dedup rows, backfill missing `content_ref`. Run after `/mega-db-prune` or a crash. |
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
| `/mega-dashboard` | Start the **localhost-only** live dashboard and open it in a browser (token gauge, store stats, live event stream, per-repo + cross-repo drift). |
|
|
248
|
-
| `/mega-dashboard-status` | Report dashboard server status. |
|
|
262
|
+
| `/mega-dashboard [open]` | Start the **localhost-only** live dashboard and open it in a browser (token gauge, store stats, live event stream, per-repo + All-repos/Summary views, cross-repo drift). |
|
|
263
|
+
| `/mega-dashboard-status` | Report dashboard server status (port / url / live). |
|
|
249
264
|
| `/mega-dashboard-stop` | Stop the dashboard server. |
|
|
250
265
|
|
|
266
|
+
### The tier system
|
|
267
|
+
|
|
268
|
+
The **tier** you see in the toolbar and dashboard is a *live pressure band* (`low` → `medium` → `high` → `ultra` → `mega`) that climbs automatically as your context window fills and falls back as it's relieved — it is driven by `currentTokens / effectiveThreshold`, not a manual setting. The base compaction *threshold* is set by `MEGACOMPACT_TIER` at startup as a **% of the model context window** (`low` 50% · `medium` 60% · `high` 70% · `ultra` 70% · `mega` 75%; default `low`) — the fire point is `tierPct × contextWindow`, so it always lands below pi's native ~80% auto-compaction (any model size). The old static token amounts (50k/100k/200k/1M/10M) are now only the boot fallback used before the first context event reports a window. `/mega-tier` was removed in v0.7.6. Higher pressure also deepens the live trim and reviews durable memory more often — the whole system reacts as one.
|
|
269
|
+
|
|
270
|
+
|
|
251
271
|
### Live stats widget
|
|
252
272
|
|
|
253
273
|
Above the pi editor the extension shows a compact widget:
|
|
254
274
|
|
|
255
275
|
```
|
|
256
|
-
⚡ high·low v0.
|
|
276
|
+
⚡ high·low v0.7.7 │ 142k/200k tokens (71%) │ 3 chkpts │ 🤖 2 agents │ turn 5
|
|
257
277
|
◐ armed │ dedup: 92% │ saved: 45k tok
|
|
258
278
|
```
|
|
259
279
|
|
|
@@ -272,7 +292,7 @@ Above the pi editor the extension shows a compact widget:
|
|
|
272
292
|
> **auto-maintenance** pass runs on `session_start`: it prunes rows older than
|
|
273
293
|
> 30d, checkpoints the WAL if it's over 10 MB, and VACUUMs if the DB is over
|
|
274
294
|
> 100 MB AND the freelist is >20% of pages. It never blocks session start and
|
|
275
|
-
> logs a one-line summary to the diagnostic log. (v0.7.
|
|
295
|
+
> logs a one-line summary to the diagnostic log. (v0.7.6+)
|
|
276
296
|
|
|
277
297
|
---
|
|
278
298
|
|
|
@@ -281,6 +301,8 @@ Above the pi editor the extension shows a compact widget:
|
|
|
281
301
|
All defaults are in `src/config/dedup.ts` (single source of truth). Set env vars
|
|
282
302
|
before starting pi.
|
|
283
303
|
|
|
304
|
+
### Core settings
|
|
305
|
+
|
|
284
306
|
| Variable | Default | Meaning |
|
|
285
307
|
|---|---|---|
|
|
286
308
|
| `MEGACOMPACT_FAST_GATE_PCT` | `70` | Context-usage % that arms the auto-trigger. Defaults to the tier's % of window (`tierPct*100`): low 50 · med 60 · high 70 · ultra 70 · mega 75. Override raises the arming floor. |
|
|
@@ -294,7 +316,7 @@ before starting pi.
|
|
|
294
316
|
| `MEGACOMPACT_DEDUP_SIM` | `0.90` | Cosine threshold to collapse near-dupes. |
|
|
295
317
|
| `MEGACOMPACT_STATE_DIR` | _(none — per-repo default)_ | Override the store location. By default state is per-repo at `<repo>/.pi/mega-compact/`; this env var forces a single explicit dir (used as the fallback for non-git cwds). |
|
|
296
318
|
|
|
297
|
-
|
|
319
|
+
### Dedup pipeline flags
|
|
298
320
|
|
|
299
321
|
These gate the L0/L1/L2/RAPTOR dedup tiers. Defaults reproduce the all-active
|
|
300
322
|
behavior. `MARK_ONLY_*` tiers run + record their decision but never
|
|
@@ -324,7 +346,7 @@ See `docs/DEDUP_RUNBOOK.md` for incident response (SEV tiers, first-15-min
|
|
|
324
346
|
checklist, MARK_ONLY degrade) and `docs/RETENTION_POLICY.md` for TTL / soft-delete
|
|
325
347
|
/ VACUUM.
|
|
326
348
|
|
|
327
|
-
|
|
349
|
+
### Continuity & memory
|
|
328
350
|
|
|
329
351
|
| Variable | Default | Meaning |
|
|
330
352
|
|---|---|---|
|
|
@@ -335,17 +357,84 @@ checklist, MARK_ONLY degrade) and `docs/RETENTION_POLICY.md` for TTL / soft-dele
|
|
|
335
357
|
| `MEGACOMPACT_MEMORY_REVIEW_INTERVAL` | `10` | Turns between auto-review cycles. |
|
|
336
358
|
| `MEGACOMPACT_PGLITE_DISABLED` | _(unset — index on)_ | Kill-switch for the PGlite/HNSW cross-repo index; set `1`/`true` to disable (falls back to sync per-session scan). |
|
|
337
359
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
The localhost-only dashboard
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
360
|
+
## Dashboard
|
|
361
|
+
|
|
362
|
+
The localhost-only dashboard (started with `/mega-dashboard`) is a single-page
|
|
363
|
+
app served from a detached child process on `127.0.0.1` (random port in
|
|
364
|
+
9320–9329). Every API is read-only — the server never writes the index or your
|
|
365
|
+
store. It reads the machine-wide `repo_registry`
|
|
366
|
+
(`~/.mega-compact-index/index.sqlite`) plus the current repo's own `node:sqlite`
|
|
367
|
+
store.
|
|
368
|
+
|
|
369
|
+
### Tabs
|
|
370
|
+
|
|
371
|
+
- **Current repo** — the live single-session view: context-window gauge, trigger
|
|
372
|
+
status, the Vector Store (checkpoints / dropped / kept / freed / injected /
|
|
373
|
+
dedup / collapsed), the repo-wide aggregate, the Data Safety card, the live
|
|
374
|
+
Model & Cost Savings card, and crew/agent activity.
|
|
375
|
+
- **All repos** — a machine-wide table aggregated from `repo_registry`
|
|
376
|
+
(`GET /api/index`): one row per repo with checkpoints, tokens saved,
|
|
377
|
+
compressed-originals, last-compacted, and active model. Each row opens a
|
|
378
|
+
per-repo detail modal. Currently-open sessions are badged **active** (see
|
|
379
|
+
below).
|
|
380
|
+
- **Summary** — machine-wide header tiles plus a **savings-by-model** table
|
|
381
|
+
(tokens in/out/freed, context window, $ saved) grouped by the model you were
|
|
382
|
+
running.
|
|
383
|
+
|
|
384
|
+
### Active Repos tab
|
|
385
|
+
|
|
386
|
+
The dashboard has a dedicated **Active Repos** tab that lists every server /
|
|
387
|
+
session seen within the **last 30 minutes**, each with its live tier,
|
|
388
|
+
context %, and session state. It is backed by `GET /api/servers`, which walks
|
|
389
|
+
the machine-wide `repo_registry`, reads each repo's per-process
|
|
390
|
+
`dashboard.json` snapshot, and returns one row per currently-open session —
|
|
391
|
+
so 1–6 sessions running at once are visible together in a single table instead
|
|
392
|
+
of only the single current-repo view. Each row shows that repo's live
|
|
393
|
+
cache-hit / compaction / time-saved totals (see below).
|
|
394
|
+
|
|
395
|
+
The older **All repos** view and `GET /api/summary` still surface an
|
|
396
|
+
`activeRepos` count, and `GET /api/repos?active=Nh` filters to repos seen
|
|
397
|
+
within the last *N* hours (e.g. `?active=24h`, hour-granular) for the
|
|
398
|
+
longer-window cross-repo table.
|
|
399
|
+
|
|
400
|
+
### Metrics (DB-backed, durable)
|
|
401
|
+
|
|
402
|
+
The dashboard's cumulative **Cache hits**, **Compactions**, and **Estimated
|
|
403
|
+
time saved** cards are backed by **SQLite `meta` counters**
|
|
404
|
+
(`compact_count`, `recall_injected`, `cache_hit_tokens_saved`, plus the
|
|
405
|
+
existing `tokens_saved` / `deduped`), not the per-process `dashboard.json`
|
|
406
|
+
snapshot. Because they live in the repo's `node:sqlite` store, the totals are
|
|
407
|
+
**durable across session restarts** and travel with the repo's state dir —
|
|
408
|
+
`dashboard.json` is now just the live per-process view that feeds the Active
|
|
409
|
+
Repos rows. The cards show:
|
|
410
|
+
|
|
411
|
+
- **Cache hits** = dedup collapses (`deduped`) + recall re-injections
|
|
412
|
+
(`recall_injected`) — as **current session** and **repo-wide total**.
|
|
413
|
+
- **Compactions** = current session (`checkpointCount`) + repo-wide total
|
|
414
|
+
(`compact_count` from `meta`).
|
|
415
|
+
- **Estimated time saved** = compact time saved + cache-hit time saved, derived
|
|
416
|
+
from tokens ÷ ~2k tok/s and labeled `est.`, as current session + total.
|
|
417
|
+
|
|
418
|
+
### Localhost API
|
|
419
|
+
|
|
420
|
+
`GET /api/snapshot` (current-repo live state),
|
|
421
|
+
`/api/servers` (**Active Repos** — sessions active in the last 30 min, with
|
|
422
|
+
tier / context % / state / live cache-hit & compaction totals),
|
|
423
|
+
`/api/index` (all repos), `/api/repos` (with `?active=Nh` filter), `/api/summary`
|
|
424
|
+
(header tiles + `activeRepos`), `/api/drift` (cross-repo drift: stale /
|
|
425
|
+
compaction-lag / model-churn — read-only), `/api/events` (SSE live event stream),
|
|
426
|
+
and `/api/version`.
|
|
427
|
+
|
|
428
|
+
### Data safety
|
|
429
|
+
|
|
430
|
+
Every compacted region is kept verbatim (compressed);
|
|
431
|
+
the Data Safety card shows regions retained, compressed-originals bytes, dedup
|
|
432
|
+
duplicates, and permanently-deleted bytes (**always 0**). Nothing is permanently
|
|
433
|
+
deleted — any region is restorable.
|
|
345
434
|
|
|
346
435
|
---
|
|
347
436
|
|
|
348
|
-
## Architecture
|
|
437
|
+
## Architecture
|
|
349
438
|
|
|
350
439
|
```
|
|
351
440
|
extensions/mega-compact.ts pi extension entry; wires src/ into pi lifecycle
|
|
@@ -385,7 +474,7 @@ The extension entry adapts between the engine and pi's runtime types.
|
|
|
385
474
|
|
|
386
475
|
```bash
|
|
387
476
|
npm run build # tsc
|
|
388
|
-
npm test # build + node --test on dist/**/*.test.js (
|
|
477
|
+
npm test # build + node --test on dist/**/*.test.js (407 tests)
|
|
389
478
|
npm run lint # tsc --noEmit + guardrails-scan
|
|
390
479
|
npm run guardrails # regression_check + guardrails-scan
|
|
391
480
|
```
|
|
@@ -45,6 +45,7 @@ function log(...parts) {
|
|
|
45
45
|
// concurrent writer's WAL never blocks the request). All registry data lives in
|
|
46
46
|
// SQLite (the project's one-store invariant) — there is no JSON mirror. Same
|
|
47
47
|
// index-dir resolution as src/store/sqlite.ts getIndexDir().
|
|
48
|
+
const ACTIVE_WINDOW_SEC = 1800;
|
|
48
49
|
function getIndexDir() {
|
|
49
50
|
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
50
51
|
if (override && override.trim() !== "")
|
|
@@ -169,9 +170,6 @@ function readIndex() {
|
|
|
169
170
|
// ---------------------------------------------------------------------------
|
|
170
171
|
/** Package version of this extension, surfaced in the dashboard header. */
|
|
171
172
|
let dashboardServerVersion = "0.0.0";
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Helpers
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
173
|
function readSnapshot(snapshotPath) {
|
|
176
174
|
try {
|
|
177
175
|
const raw = readFileSync(snapshotPath, "utf-8");
|
|
@@ -192,6 +190,9 @@ function readSnapshot(snapshotPath) {
|
|
|
192
190
|
crew: { activeAgents: 0, currentTurn: 0 },
|
|
193
191
|
repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
|
|
194
192
|
integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
|
|
193
|
+
cacheHits: { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 },
|
|
194
|
+
compacts: { session: 0, total: 0 },
|
|
195
|
+
timeSaved: { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } },
|
|
195
196
|
compression: { session: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 }, repo: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 } },
|
|
196
197
|
model: undefined,
|
|
197
198
|
};
|
|
@@ -315,6 +316,7 @@ function dashboardHtml(tierName) {
|
|
|
315
316
|
<nav class="tabs">
|
|
316
317
|
<button class="tab active" data-tab="current">Current repo</button>
|
|
317
318
|
<button class="tab" data-tab="all">All repos</button>
|
|
319
|
+
<button class="tab" data-tab="active">Active Repos</button>
|
|
318
320
|
<button class="tab" data-tab="summary">Summary</button>
|
|
319
321
|
</nav>
|
|
320
322
|
|
|
@@ -416,6 +418,26 @@ function dashboardHtml(tierName) {
|
|
|
416
418
|
</ul>
|
|
417
419
|
<p class="legend-note">Hover any label above for a quick explanation.</p>
|
|
418
420
|
</div>
|
|
421
|
+
<div class="card">
|
|
422
|
+
<h2>💾 Cache Hits & Compactions</h2>
|
|
423
|
+
<div class="stat-grid">
|
|
424
|
+
<span class="label">Cache Hits (session)</span><span class="value" id="ch-session">0</span>
|
|
425
|
+
<span class="label">Cache Hits (total)</span><span class="value" id="ch-total">0</span>
|
|
426
|
+
<span class="label">Tokens Saved (session)</span><span class="value" id="ch-tok-session">0</span>
|
|
427
|
+
<span class="label">Tokens Saved (total)</span><span class="value" id="ch-tok-total">0</span>
|
|
428
|
+
<span class="label">Compactions (session)</span><span class="value" id="cp-session">0</span>
|
|
429
|
+
<span class="label">Compactions (total)</span><span class="value" id="cp-total">0</span>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="card">
|
|
433
|
+
<h2>⏱ Time Saved (est.)</h2>
|
|
434
|
+
<div class="stat-grid">
|
|
435
|
+
<span class="label">Compact (session)</span><span class="value" id="ts-compact-session">0</span>
|
|
436
|
+
<span class="label">Compact (total)</span><span class="value" id="ts-compact-total">0</span>
|
|
437
|
+
<span class="label">Cache Hit (session)</span><span class="value" id="ts-cache-session">0</span>
|
|
438
|
+
<span class="label">Cache Hit (total)</span><span class="value" id="ts-cache-total">0</span>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
419
441
|
</div>
|
|
420
442
|
|
|
421
443
|
<div class="events">
|
|
@@ -441,6 +463,28 @@ function dashboardHtml(tierName) {
|
|
|
441
463
|
<div class="updated" id="updated"></div>
|
|
442
464
|
</div><!-- /panel-current -->
|
|
443
465
|
|
|
466
|
+
<!-- Active repos (live cache-hit / compaction stats across machines) -->
|
|
467
|
+
<div class="tab-panel" id="panel-active">
|
|
468
|
+
<div class="card">
|
|
469
|
+
<h2>Active Repos — Live Cache Hits & Compactions</h2>
|
|
470
|
+
<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>
|
|
471
|
+
<table class="repos">
|
|
472
|
+
<thead>
|
|
473
|
+
<tr>
|
|
474
|
+
<th>Repo</th><th>Model</th><th>Tier</th>
|
|
475
|
+
<th style="text-align:right">Context %</th><th>State</th>
|
|
476
|
+
<th style="text-align:right">Compactions (s/t)</th>
|
|
477
|
+
<th style="text-align:right">Cache Hits (s/t)</th>
|
|
478
|
+
<th style="text-align:right">Compact s/t (s)</th>
|
|
479
|
+
<th style="text-align:right">CacheHit s/t (s)</th>
|
|
480
|
+
</tr>
|
|
481
|
+
</thead>
|
|
482
|
+
<tbody id="active-rows"><tr><td colspan="9" class="repo-none">loading…</td></tr></tbody>
|
|
483
|
+
</table>
|
|
484
|
+
<div class="updated" id="active-updated"></div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
444
488
|
<!-- Per-repo detail modal -->
|
|
445
489
|
<div class="repo-detail" id="repo-detail">
|
|
446
490
|
<div class="repo-detail-box">
|
|
@@ -635,6 +679,21 @@ function dashboardHtml(tierName) {
|
|
|
635
679
|
document.getElementById('cost-windows').textContent = '0 context-windows extended';
|
|
636
680
|
}
|
|
637
681
|
|
|
682
|
+
// --- Cache hits & compactions (live counters) ---------------------------
|
|
683
|
+
var ch = d.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
|
|
684
|
+
var cp = d.compacts || { session: 0, total: 0 };
|
|
685
|
+
var ts = d.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
|
|
686
|
+
document.getElementById('ch-session').textContent = (ch.session || 0).toLocaleString();
|
|
687
|
+
document.getElementById('ch-total').textContent = (ch.total || 0).toLocaleString();
|
|
688
|
+
document.getElementById('ch-tok-session').textContent = (ch.sessionTokensSaved || 0).toLocaleString();
|
|
689
|
+
document.getElementById('ch-tok-total').textContent = (ch.totalTokensSaved || 0).toLocaleString();
|
|
690
|
+
document.getElementById('cp-session').textContent = (cp.session || 0).toLocaleString();
|
|
691
|
+
document.getElementById('cp-total').textContent = (cp.total || 0).toLocaleString();
|
|
692
|
+
document.getElementById('ts-compact-session').textContent = fmtSec(ts.compact.sessionSec);
|
|
693
|
+
document.getElementById('ts-compact-total').textContent = fmtSec(ts.compact.totalSec);
|
|
694
|
+
document.getElementById('ts-cache-session').textContent = fmtSec(ts.cacheHit.sessionSec);
|
|
695
|
+
document.getElementById('ts-cache-total').textContent = fmtSec(ts.cacheHit.totalSec);
|
|
696
|
+
|
|
638
697
|
document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
|
|
639
698
|
}
|
|
640
699
|
|
|
@@ -840,9 +899,51 @@ function dashboardHtml(tierName) {
|
|
|
840
899
|
pollIndex();
|
|
841
900
|
setInterval(pollIndex, 5000);
|
|
842
901
|
|
|
902
|
+
// --- Active repos (live cache-hit / compaction stats) ---------------------
|
|
903
|
+
function fmtSec(s) {
|
|
904
|
+
s = s || 0;
|
|
905
|
+
if (s >= 3600) return (s / 3600).toFixed(1) + 'h';
|
|
906
|
+
if (s >= 60) return Math.round(s / 60) + 'm';
|
|
907
|
+
if (s >= 1) return s.toFixed(1) + 's';
|
|
908
|
+
return Math.round(s * 1000) + 'ms';
|
|
909
|
+
}
|
|
910
|
+
function renderActiveRepos(d) {
|
|
911
|
+
d = d || { updatedAt: null, servers: [] };
|
|
912
|
+
var servers = d.servers || [];
|
|
913
|
+
var rowsEl = document.getElementById('active-rows');
|
|
914
|
+
if (!rowsEl) return;
|
|
915
|
+
if (!servers.length) {
|
|
916
|
+
rowsEl.innerHTML = '<tr><td colspan="9" class="repo-none">No active repositories.</td></tr>';
|
|
917
|
+
} else {
|
|
918
|
+
rowsEl.innerHTML = servers.map(function(r) {
|
|
919
|
+
var ch = r.cacheHits || { session: 0, total: 0, sessionTokensSaved: 0, totalTokensSaved: 0 };
|
|
920
|
+
var cp = r.compacts || { session: 0, total: 0 };
|
|
921
|
+
var ts = r.timeSaved || { compact: { sessionSec: 0, totalSec: 0 }, cacheHit: { sessionSec: 0, totalSec: 0 } };
|
|
922
|
+
return '<tr>' +
|
|
923
|
+
'<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
|
|
924
|
+
'<td>' + sanitize(r.model || '—') + '</td>' +
|
|
925
|
+
'<td>' + sanitize(r.tier || '—') + '</td>' +
|
|
926
|
+
'<td class="num">' + (r.contextPct != null ? Math.round(r.contextPct * 100) + '%' : '—') + '</td>' +
|
|
927
|
+
'<td>' + sanitize(r.state || '—') + '</td>' +
|
|
928
|
+
'<td class="num">' + (cp.session || 0) + ' / ' + (cp.total || 0) + '</td>' +
|
|
929
|
+
'<td class="num">' + (ch.session || 0) + ' / ' + (ch.total || 0) + '</td>' +
|
|
930
|
+
'<td class="num">' + fmtSec(ts.compact.sessionSec) + ' / ' + fmtSec(ts.compact.totalSec) + '</td>' +
|
|
931
|
+
'<td class="num">' + fmtSec(ts.cacheHit.sessionSec) + ' / ' + fmtSec(ts.cacheHit.totalSec) + '</td>' +
|
|
932
|
+
'</tr>';
|
|
933
|
+
}).join('');
|
|
934
|
+
}
|
|
935
|
+
var upd = document.getElementById('active-updated');
|
|
936
|
+
if (upd) upd.textContent = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
|
|
937
|
+
}
|
|
938
|
+
function pollServers() {
|
|
939
|
+
fetch('/api/servers').then(function(r) { return r.json(); }).then(renderActiveRepos).catch(function() {});
|
|
940
|
+
}
|
|
941
|
+
pollServers();
|
|
942
|
+
setInterval(pollServers, 5000);
|
|
943
|
+
|
|
843
944
|
// --- Tab switching ------------------------------------------------------
|
|
844
945
|
var tabs = document.querySelectorAll('.tab');
|
|
845
|
-
var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
|
|
946
|
+
var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary' };
|
|
846
947
|
for (var i = 0; i < tabs.length; i++) {
|
|
847
948
|
tabs[i].addEventListener('click', function() {
|
|
848
949
|
var name = this.getAttribute('data-tab');
|
|
@@ -855,6 +956,7 @@ function dashboardHtml(tierName) {
|
|
|
855
956
|
}
|
|
856
957
|
}
|
|
857
958
|
if (name === 'all' || name === 'summary') pollIndex();
|
|
959
|
+
if (name === 'active') pollServers();
|
|
858
960
|
});
|
|
859
961
|
}
|
|
860
962
|
})();
|
|
@@ -1063,6 +1165,37 @@ export async function launchDashboardServer(stateDir) {
|
|
|
1063
1165
|
res.end(JSON.stringify(report));
|
|
1064
1166
|
return;
|
|
1065
1167
|
}
|
|
1168
|
+
if (req.url === "/api/servers") {
|
|
1169
|
+
try {
|
|
1170
|
+
const idx = readIndex();
|
|
1171
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
1172
|
+
const servers = (idx?.repos ?? []).filter((r) => (r.lastSeen ?? 0) >= nowSec - ACTIVE_WINDOW_SEC).map((r) => {
|
|
1173
|
+
const out = { repoRoot: r.repoRoot, displayName: r.displayName, model: r.modelName, provider: r.providerName, lastSeen: r.lastSeen, lastCompactedAt: r.lastCompactedAt };
|
|
1174
|
+
try {
|
|
1175
|
+
const p = join(r.stateDir, "dashboard.json");
|
|
1176
|
+
if (existsSync(p)) {
|
|
1177
|
+
const snap = JSON.parse(readFileSync(p, "utf-8"));
|
|
1178
|
+
out.tier = snap.tier ?? null;
|
|
1179
|
+
out.contextPct = (snap.context && snap.context.percent != null) ? snap.context.percent : null;
|
|
1180
|
+
out.state = (snap.session && snap.session.state) || null;
|
|
1181
|
+
out.cacheHits = snap.cacheHits ?? null;
|
|
1182
|
+
out.compacts = snap.compacts ?? null;
|
|
1183
|
+
out.timeSaved = snap.timeSaved ?? null;
|
|
1184
|
+
out.updatedAt = snap.updatedAt ?? null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch { /* best-effort */ }
|
|
1188
|
+
return out;
|
|
1189
|
+
}).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
1190
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1191
|
+
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), servers }));
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1195
|
+
res.end(JSON.stringify({ error: "servers_unavailable" }));
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1066
1199
|
if (req.url === "/api/events") {
|
|
1067
1200
|
res.writeHead(200, {
|
|
1068
1201
|
"Content-Type": "text/event-stream",
|
|
@@ -195,6 +195,65 @@ describe("multi-repo /api/index (S19)", () => {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
|
+
describe("multi-repo /api/servers (active cache-hit stats)", () => {
|
|
199
|
+
test("returns active repos with live dashboard.json cache-hit/compaction stats", async () => {
|
|
200
|
+
const dir = mkdtempSync(join(tmpdir(), "dash-servers-"));
|
|
201
|
+
const indexDir = mkdtempSync(join(tmpdir(), "index-servers-"));
|
|
202
|
+
process.env.MEGACOMPACT_INDEX_DIR = indexDir;
|
|
203
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = "19323";
|
|
204
|
+
const { upsertRepoRegistry } = await import("../src/store/sqlite.js");
|
|
205
|
+
const activeState = mkdtempSync(join(tmpdir(), "srv-active-"));
|
|
206
|
+
writeFileSync(join(activeState, "dashboard.json"), JSON.stringify({
|
|
207
|
+
updatedAt: new Date().toISOString(),
|
|
208
|
+
tier: "high",
|
|
209
|
+
context: { tokens: 5000, percent: 0.42, contextWindow: 12000 },
|
|
210
|
+
session: { id: "s1", state: "idle" },
|
|
211
|
+
cacheHits: { session: 3, total: 7, sessionTokensSaved: 1200, totalTokensSaved: 9000 },
|
|
212
|
+
compacts: { session: 2, total: 5 },
|
|
213
|
+
timeSaved: { compact: { sessionSec: 1.5, totalSec: 4 }, cacheHit: { sessionSec: 0.6, totalSec: 4.5 } },
|
|
214
|
+
}, null, 2));
|
|
215
|
+
upsertRepoRegistry({ 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" }, indexDir);
|
|
216
|
+
const staleState = mkdtempSync(join(tmpdir(), "srv-stale-"));
|
|
217
|
+
writeFileSync(join(staleState, "dashboard.json"), JSON.stringify({ updatedAt: new Date().toISOString(), tier: "low" }, null, 2));
|
|
218
|
+
const longAgo = Math.floor(Date.now() / 1000) - 7 * 86_400;
|
|
219
|
+
upsertRepoRegistry({ repoRoot: "/home/u/stale", displayName: "stale", stateDir: staleState, checkpointCount: 1, tokensSaved: 100, compressedOriginalBytes: 0, lastSeen: longAgo }, indexDir);
|
|
220
|
+
const child = spawn(process.execPath, [SERVER_ENTRY, dir], { stdio: "ignore" });
|
|
221
|
+
try {
|
|
222
|
+
await waitFor(async () => {
|
|
223
|
+
try {
|
|
224
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
225
|
+
const res = await fetch(`http://localhost:${raw.port}/api/version`);
|
|
226
|
+
return res.ok;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
233
|
+
const body = (await fetch(`http://localhost:${raw.port}/api/servers`).then((r) => r.json()));
|
|
234
|
+
assert.equal(body.servers.length, 1, "only the active repo is returned");
|
|
235
|
+
const s = body.servers[0];
|
|
236
|
+
assert.equal(s.displayName, "active");
|
|
237
|
+
assert.equal(s.tier, "high");
|
|
238
|
+
assert.equal(s.model, "gpt-4o");
|
|
239
|
+
assert.equal(s.provider, "OpenAI");
|
|
240
|
+
assert.equal(s.contextPct, 0.42);
|
|
241
|
+
assert.equal(s.state, "idle");
|
|
242
|
+
assert.deepEqual(s.cacheHits, { session: 3, total: 7, sessionTokensSaved: 1200, totalTokensSaved: 9000 });
|
|
243
|
+
assert.deepEqual(s.compacts, { session: 2, total: 5 });
|
|
244
|
+
const ts = s.timeSaved;
|
|
245
|
+
assert.equal(ts.compact.sessionSec, 1.5);
|
|
246
|
+
assert.equal(ts.cacheHit.sessionSec, 0.6);
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
child.kill("SIGTERM");
|
|
250
|
+
delete process.env.MEGACOMPACT_INDEX_DIR;
|
|
251
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
252
|
+
rmSync(dir, { recursive: true, force: true });
|
|
253
|
+
rmSync(indexDir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
198
257
|
// ---------------------------------------------------------------------------
|
|
199
258
|
// Lifecycle integration — launch the compiled server as a real subprocess
|
|
200
259
|
// (the same way the /dashboard command spawns it) and assert the two failure
|