pi-mega-compact 0.7.8 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/helpers.js +37 -0
  3. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  4. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  5. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  6. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  7. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  8. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  9. package/dist/extensions/dashboard-server/html/script.js +259 -0
  10. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  11. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  12. package/dist/extensions/dashboard-server/html-template.js +41 -0
  13. package/dist/extensions/dashboard-server/html.js +756 -0
  14. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  15. package/dist/extensions/dashboard-server/server.js +370 -0
  16. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  17. package/dist/extensions/dashboard-server/state.js +30 -0
  18. package/dist/extensions/dashboard-server/types.js +5 -0
  19. package/dist/extensions/dashboard-server.js +7 -1315
  20. package/dist/extensions/mega-commands.js +162 -134
  21. package/dist/extensions/mega-compact.test.js +90 -21
  22. package/dist/extensions/mega-conflict-cmds.js +5 -1
  23. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  24. package/dist/extensions/mega-db-cmds.js +11 -2
  25. package/dist/extensions/mega-events/agent-handlers.js +173 -0
  26. package/dist/extensions/mega-events/compact-handlers.js +133 -0
  27. package/dist/extensions/mega-events/context-handler.js +249 -0
  28. package/dist/extensions/mega-events/register.js +21 -0
  29. package/dist/extensions/mega-events/session-handlers.js +142 -0
  30. package/dist/extensions/mega-events.js +15 -699
  31. package/dist/extensions/mega-pipeline/compact.js +324 -0
  32. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  33. package/dist/extensions/mega-pipeline/recall.js +147 -0
  34. package/dist/extensions/mega-pipeline.js +9 -480
  35. package/dist/extensions/mega-runtime/helpers.js +40 -0
  36. package/dist/extensions/mega-runtime/query.js +29 -0
  37. package/dist/extensions/mega-runtime/state.js +711 -0
  38. package/dist/extensions/mega-runtime/widget.js +197 -0
  39. package/dist/extensions/mega-runtime.js +15 -947
  40. package/dist/src/store/sqlite/checkpoints.js +145 -0
  41. package/dist/src/store/sqlite/connection.js +35 -0
  42. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  43. package/dist/src/store/sqlite/foundation.js +38 -0
  44. package/dist/src/store/sqlite/global-index.js +224 -0
  45. package/dist/src/store/sqlite/index-store.js +167 -0
  46. package/dist/src/store/sqlite/maintenance.js +235 -0
  47. package/dist/src/store/sqlite/memories.js +164 -0
  48. package/dist/src/store/sqlite/memory.js +54 -0
  49. package/dist/src/store/sqlite/meta.js +82 -0
  50. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  51. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  52. package/dist/src/store/sqlite/raptor.js +57 -0
  53. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  54. package/dist/src/store/sqlite/schema.js +250 -0
  55. package/dist/src/store/sqlite/session-state.js +28 -0
  56. package/dist/src/store/sqlite/sessions.js +39 -0
  57. package/dist/src/store/sqlite/stats.js +66 -0
  58. package/dist/src/store/sqlite/transaction.js +19 -0
  59. package/dist/src/store/sqlite/utils.js +120 -0
  60. package/dist/src/store/sqlite.js +20 -1607
  61. package/dist/src/vectorStore/add.js +260 -0
  62. package/dist/src/vectorStore/dedup.js +52 -0
  63. package/dist/src/vectorStore/index.js +10 -0
  64. package/dist/src/vectorStore/queries.js +83 -0
  65. package/dist/src/vectorStore/search.js +95 -0
  66. package/dist/src/vectorStore/session.js +19 -0
  67. package/dist/src/vectorStore/store.js +105 -0
  68. package/dist/src/vectorStore/types.js +6 -0
  69. package/dist/src/vectorStore/utils.js +23 -0
  70. package/extensions/dashboard-server/html.ts +758 -0
  71. package/extensions/dashboard-server/index-reader.ts +130 -0
  72. package/extensions/dashboard-server/server.ts +358 -0
  73. package/extensions/dashboard-server/snapshot.ts +44 -0
  74. package/extensions/dashboard-server/state.ts +33 -0
  75. package/extensions/dashboard-server/types.ts +134 -0
  76. package/extensions/dashboard-server.ts +7 -1431
  77. package/extensions/mega-commands.ts +33 -10
  78. package/extensions/mega-compact.test.ts +198 -43
  79. package/extensions/mega-conflict-cmds.ts +6 -2
  80. package/extensions/mega-dashboard-cmds.ts +30 -23
  81. package/extensions/mega-db-cmds.ts +11 -3
  82. package/extensions/mega-events/agent-handlers.ts +214 -0
  83. package/extensions/mega-events/compact-handlers.ts +164 -0
  84. package/extensions/mega-events/context-handler.ts +290 -0
  85. package/extensions/mega-events/register.ts +37 -0
  86. package/extensions/mega-events/session-handlers.ts +165 -0
  87. package/extensions/mega-events.ts +15 -780
  88. package/extensions/mega-pipeline/compact.ts +366 -0
  89. package/extensions/mega-pipeline/memory-review.ts +46 -0
  90. package/extensions/mega-pipeline/recall.ts +165 -0
  91. package/extensions/mega-pipeline.ts +9 -537
  92. package/extensions/mega-runtime/helpers.ts +68 -0
  93. package/extensions/mega-runtime/query.ts +29 -0
  94. package/extensions/mega-runtime/state.ts +797 -0
  95. package/extensions/mega-runtime/widget.ts +258 -0
  96. package/extensions/mega-runtime.ts +15 -1093
  97. package/package.json +4 -3
  98. package/src/store/sqlite/checkpoints.ts +204 -0
  99. package/src/store/sqlite/dedup-mirror.ts +114 -0
  100. package/src/store/sqlite/foundation.ts +63 -0
  101. package/src/store/sqlite/global-index.ts +305 -0
  102. package/src/store/sqlite/maintenance.ts +294 -0
  103. package/src/store/sqlite/memories.ts +217 -0
  104. package/src/store/sqlite/meta.ts +108 -0
  105. package/src/store/sqlite/model-snapshots.ts +83 -0
  106. package/src/store/sqlite/raptor.ts +107 -0
  107. package/src/store/sqlite/raw-transcript.ts +221 -0
  108. package/src/store/sqlite/schema.ts +258 -0
  109. package/src/store/sqlite/session-state.ts +38 -0
  110. package/src/store/sqlite/stats.ts +127 -0
  111. package/src/store/sqlite/utils.ts +125 -0
  112. package/src/store/sqlite.ts +20 -2204
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dashboard-server.ts — lightweight local web dashboard for mega-compact.
2
+ * dashboard-server.ts — barrel file re-exporting all dashboard server submodules.
3
3
  *
4
4
  * Zero npm dependencies. Uses only Node built-in modules (http, fs, path).
5
5
  * Serves a single-page HTML dashboard, a JSON snapshot API, and an SSE
@@ -10,1317 +10,9 @@
10
10
  *
11
11
  * @module
12
12
  */
13
- import { createServer } from "node:http";
14
- import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync, appendFileSync } from "node:fs";
15
- import { homedir } from "node:os";
16
- import { join, dirname } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import { createRequire } from "node:module";
19
- import { DatabaseSync } from "node:sqlite";
20
- // ---------------------------------------------------------------------------
21
- // Local runtime log
22
- //
23
- // The dashboard server is spawned as a DETACHED child. When it is launched with
24
- // `stdio: "ignore"` (the old default) any crash before the first console.log is
25
- // invisible — there is no log to "check". We therefore mirror every lifecycle
26
- // line to a file in the state dir so a failed start is always diagnosable. The
27
- // launcher also captures stderr, so this doubles as defense-in-depth.
28
- // ---------------------------------------------------------------------------
29
- let LOG_PATH = null;
30
- function log(...parts) {
31
- const line = `[mega-compact][dashboard] ${parts.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")}`;
32
- // eslint-disable-next-line no-console
33
- console.error(line); // stderr — captured by the launcher pipe
34
- if (LOG_PATH) {
35
- try {
36
- appendFileSync(LOG_PATH, new Date().toISOString() + " " + line + "\n");
37
- }
38
- catch { /* non-fatal */ }
39
- }
40
- }
41
- // --- Multi-repo index (Phase 5b) ------------------------------------------------
42
- // The extension writes a machine-wide repo registry into a single SQLite DB
43
- // (<indexDir>/index.sqlite) as the concurrency-safe write path; the dashboard
44
- // reads that table directly (one read-only connection, opened per request so a
45
- // concurrent writer's WAL never blocks the request). All registry data lives in
46
- // SQLite (the project's one-store invariant) — there is no JSON mirror. Same
47
- // index-dir resolution as src/store/sqlite.ts getIndexDir().
48
- const ACTIVE_WINDOW_SEC = 1800;
49
- function getIndexDir() {
50
- const override = process.env.MEGACOMPACT_INDEX_DIR;
51
- if (override && override.trim() !== "")
52
- return override;
53
- try {
54
- return join(homedir(), ".mega-compact-index");
55
- }
56
- catch {
57
- return join("/tmp", ".mega-compact-index");
58
- }
59
- }
60
- /** Read the machine-wide repo registry from SQLite (read-only, single shot). */
61
- function readIndex() {
62
- const indexPath = join(getIndexDir(), "index.sqlite");
63
- if (!existsSync(indexPath))
64
- return null;
65
- let db;
66
- try {
67
- // Read-only + immutable WAL so a concurrent writer's WAL never blocks us.
68
- db = new DatabaseSync(indexPath, { readOnly: true });
69
- db.exec("PRAGMA journal_mode = WAL");
70
- const rows = db
71
- .prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
72
- .all();
73
- const mapped = rows.map((r) => ({
74
- repoRoot: String(r.repo_root ?? ""),
75
- displayName: String(r.display_name ?? ""),
76
- stateDir: String(r.state_dir ?? ""),
77
- checkpointCount: Number(r.checkpoint_count ?? 0),
78
- tokensSaved: Number(r.tokens_saved ?? 0),
79
- compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
80
- lastCompactedAt: r.last_compacted_at ?? null,
81
- provider: r.provider ?? null,
82
- providerName: r.provider_name ?? null,
83
- modelName: r.model_name ?? null,
84
- inputRate: r.input_rate ?? null,
85
- outputRate: r.output_rate ?? null,
86
- lastSeen: Number(r.last_seen ?? 0),
87
- // Defaults — enriched below from each repo's own store.
88
- tokensKept: 0,
89
- tokensDropped: 0,
90
- sessions: 0,
91
- contextWindow: null,
92
- maxTokens: null,
93
- reasoning: null,
94
- }));
95
- // Enrich each repo with per-store token + model detail read directly via
96
- // node:sqlite (same zero-dependency invariant as readIndex; no store graph
97
- // import). Best-effort: a missing/corrupt store degrades to the defaults
98
- // above so the dashboard never fails to render.
99
- for (const repo of mapped) {
100
- try {
101
- const storePath = join(repo.stateDir, "sqlite.db");
102
- if (existsSync(storePath)) {
103
- const sdb = new DatabaseSync(storePath, { readOnly: true });
104
- try {
105
- const tok = sdb
106
- .prepare(`SELECT COALESCE(SUM(token_estimate),0) AS kept,
107
- COALESCE(SUM(original_token_estimate),0) AS dropped,
108
- COUNT(DISTINCT session_id) AS sess
109
- FROM context_chunks WHERE dedup_status != 'removed'`)
110
- .get();
111
- repo.tokensKept = Number(tok.kept ?? 0);
112
- repo.tokensDropped = Number(tok.dropped ?? 0);
113
- repo.sessions = Number(tok.sess ?? 0);
114
- const mrow = sdb
115
- .prepare(`SELECT context_window, max_tokens, reasoning
116
- FROM model_snapshots ORDER BY captured_at DESC LIMIT 1`)
117
- .get();
118
- if (mrow) {
119
- repo.contextWindow = Number(mrow.context_window ?? 0) || null;
120
- repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
121
- repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
122
- }
123
- }
124
- finally {
125
- sdb.close();
126
- }
127
- }
128
- }
129
- catch {
130
- /* best-effort — keep the defaults */
131
- }
132
- }
133
- // Defensive display hygiene (belt-and-suspenders — the real fix is that
134
- // tests now isolate via MEGACOMPACT_INDEX_DIR): drop transient test/temp
135
- // paths that should never have been real repos, and collapse duplicate
136
- // display names to the most-recently-seen row (rows are last_seen DESC, so
137
- // the first occurrence wins). Keeps the All-repos list readable.
138
- const isTransient = (p) => /^\/tmp\//.test(p) || /^\/private\/tmp\//.test(p) || /^\/var\/folders\//.test(p) ||
139
- /\/mc-(ext|e2e|resume|recall)-/.test(p);
140
- const seenName = new Set();
141
- const repos = [];
142
- for (const r of mapped) {
143
- if (isTransient(r.repoRoot))
144
- continue;
145
- if (seenName.has(r.displayName))
146
- continue;
147
- seenName.add(r.displayName);
148
- repos.push(r);
149
- }
150
- const summary = {
151
- totalRepos: repos.length,
152
- totalCheckpoints: repos.reduce((a, r) => a + r.checkpointCount, 0),
153
- totalTokensSaved: repos.reduce((a, r) => a + r.tokensSaved, 0),
154
- totalCompressedOriginalBytes: repos.reduce((a, r) => a + r.compressedOriginalBytes, 0),
155
- };
156
- return { updatedAt: new Date().toISOString(), summary, repos };
157
- }
158
- catch {
159
- return null;
160
- }
161
- finally {
162
- try {
163
- db?.close();
164
- }
165
- catch { /* ignore */ }
166
- }
167
- }
168
- // ---------------------------------------------------------------------------
169
- // Types
170
- // ---------------------------------------------------------------------------
171
- /** Package version of this extension, surfaced in the dashboard header. */
172
- let dashboardServerVersion = "0.0.0";
173
- function readSnapshot(snapshotPath) {
174
- try {
175
- const raw = readFileSync(snapshotPath, "utf-8");
176
- return JSON.parse(raw);
177
- }
178
- catch {
179
- return {
180
- version: 1,
181
- updatedAt: null,
182
- tier: "unknown",
183
- presetTier: "unknown",
184
- pressure: 0,
185
- config: { fastGatePct: 80, thresholdTokens: 100_000, tierPct: null, effectiveThresholdPct: null, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
186
- session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
187
- context: { tokens: null, percent: null, contextWindow: 0 },
188
- trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80, tierPct: null, effectiveThresholdPct: null },
189
- store: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
190
- crew: { activeAgents: 0, currentTurn: 0 },
191
- repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
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 } },
196
- compression: { session: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 }, repo: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 } },
197
- model: undefined,
198
- };
199
- }
200
- }
201
- function readFrom(path, charOffset) {
202
- try {
203
- const content = readFileSync(path, "utf-8");
204
- if (content.length <= charOffset)
205
- return { data: "", offset: charOffset };
206
- return { data: content.slice(charOffset), offset: content.length };
207
- }
208
- catch {
209
- return { data: "", offset: charOffset };
210
- }
211
- }
212
- // ---------------------------------------------------------------------------
213
- // HTML template
214
- // ---------------------------------------------------------------------------
215
- function dashboardHtml(tierName) {
216
- return `<!DOCTYPE html>
217
- <html lang="en">
218
- <head>
219
- <meta charset="utf-8">
220
- <meta name="viewport" content="width=device-width, initial-scale=1">
221
- <title>mega-compact dashboard</title>
222
- <style>
223
- * { margin: 0; padding: 0; box-sizing: border-box; }
224
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
225
- h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
226
- h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
227
- h1 .version-pill { background: #30363d; color: #8b949e; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
228
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
229
- .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
230
- .card.safe { border-color: #238636; }
231
- .card.safe h2 { color: #3fb950; }
232
- .safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
233
- .value.ok { color: #3fb950; }
234
- .label {
235
- cursor: help;
236
- border-bottom: 1px dotted #484f58;
237
- }
238
- .card.legend { grid-column: 1 / -1; }
239
- .legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
240
- .legend-list li { margin-bottom: 8px; line-height: 1.5; }
241
- .legend-list b { color: #f0f6fc; }
242
- .legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
243
- .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
244
- .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
245
- .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
246
- .meter-green { background: #238636; }
247
- .meter-yellow { background: #d29922; }
248
- .meter-red { background: #f85149; }
249
- .meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
250
- .meter-sub { font-size: 12px; color: #8b949e; }
251
- .status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
252
- .status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
253
- .bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
254
- .bullet-off { background: #484f58; }
255
- .bullet-na { background: #d29922; }
256
- .state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
257
- .stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
258
- .stat-grid .label { color: #8b949e; }
259
- .stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
260
- .conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
261
- .conf-grid .label { color: #8b949e; }
262
- .conf-grid .value { color: #f0f6fc; font-family: monospace; }
263
- .events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
264
- .events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
265
- .events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
266
- .ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
267
- .ev:last-child { border-bottom: none; }
268
- .ev-type { font-weight: 700; min-width: 70px; text-align: right; }
269
- .ev-type-compact { color: #3fb950; }
270
- .ev-type-recall { color: #a371f7; }
271
- .ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
272
- .ev-detail { color: #8b949e; flex: 1; }
273
- .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
274
- .empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
275
- .offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
276
- .tabs { display: flex; gap: 8px; margin-bottom: 20px; }
277
- .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; }
278
- .tab:hover { color: #c9d1d9; border-color: #484f58; }
279
- .tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
280
- .tab-panel { display: none; }
281
- .tab-panel.active { display: block; }
282
- .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
283
- .summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
284
- .summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
285
- .summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
286
- table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
287
- table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
288
- table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
289
- table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
290
- table.repos tr:last-child td { border-bottom: none; }
291
- table.repos tr:hover td { background: #1c2128; }
292
- .repo-model { color: #a371f7; }
293
- .repo-none { color: #484f58; font-style: italic; }
294
- .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
295
- .model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
296
- .card.cost h2 { color: #a371f7; }
297
- .cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
298
- .cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
299
- .repo-link { cursor: pointer; }
300
- .repo-link:hover td { color: #58a6ff; }
301
- .repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
302
- .repo-detail.open { display: flex; }
303
- .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; }
304
- .repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
305
- .repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
306
- .repo-close:hover { color: #f0f6fc; }
307
- .repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
308
- </style>
309
- </head>
310
- <body>
311
-
312
- <div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
313
-
314
- <h1><span>mega-compact</span><span class="tier" id="hdr-tier">${tierName}</span><span class="version-pill">v${dashboardServerVersion}</span><span class="model-pill" id="hdr-model">—</span></h1>
315
-
316
- <nav class="tabs">
317
- <button class="tab active" data-tab="current">Current repo</button>
318
- <button class="tab" data-tab="all">All repos</button>
319
- <button class="tab" data-tab="active">Active Repos</button>
320
- <button class="tab" data-tab="summary">Summary</button>
321
- </nav>
322
-
323
- <!-- Current repo (existing single-repo view) -->
324
- <div class="tab-panel" id="panel-current">
325
- <div class="grid">
326
- <div class="card">
327
- <h2>Context Window</h2>
328
- <div class="meter-label" id="ctx-pct">—</div>
329
- <div class="meter-track"><div class="meter-fill" id="ctx-bar" style="width:0%"></div></div>
330
- <div class="meter-sub" id="ctx-sub">waiting for data</div>
331
- </div>
332
- <div class="card">
333
- <h2>Trigger Status</h2>
334
- <div class="status-row"><div class="bullet" id="tr-armed"></div><span>Armed (context ≥ fast gate)</span></div>
335
- <div class="status-row"><div class="bullet" id="tr-ready"></div><span>Ready (tokens ≥ threshold)</span></div>
336
- <div class="state-text" id="tr-state">waiting</div>
337
- </div>
338
- <div class="card">
339
- <h2>Vector Store</h2>
340
- <div class="stat-grid">
341
- <span class="label" title="A saved summary of a chunk of your conversation that was compacted to free up space.">Checkpoints</span><span class="value" id="st-count">0</span>
342
- <span class="label" title="Total size of the original conversation text dropped into compaction this session, including redundant regions skipped by dedup. This is the 'in'.">Original (dropped)</span><span class="value" id="st-in">0</span>
343
- <span class="label" title="Compact summaries we are currently holding as 'memory' for this session (the 'out'). Smaller is better.">Kept (summaries)</span><span class="value" id="st-kept">0</span>
344
- <span class="label" title="Conversation space freed = dropped − kept (the 'saved').">Freed (dropped − kept)</span><span class="value" id="st-freed">0</span>
345
- <span class="label" title="How many times old context was automatically brought back into the conversation because it was relevant to what you were doing.">Injected</span><span class="value" id="st-injected">0</span>
346
- <span class="label" title="Of the times we recalled old context, how often it was actually on-topic.">Recall Relevance</span><span class="value" id="st-dedup">0%</span>
347
- <span class="label" title="How often new content matched something we already had, so we skipped storing a duplicate copy. Higher = less wasted space.">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
348
- <span class="label" title="How many duplicate chunks we collapsed into one instead of storing separately.">Collapsed</span><span class="value" id="st-collapsed">0</span>
349
- <span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
350
- </div>
351
- <div class="meter-track" style="margin-top:10px"><div class="meter-fill" id="st-compress-bar" style="width:0%"></div></div>
352
- <div class="meter-sub" id="st-compress-sub">waiting for compaction…</div>
353
- </div>
354
- <div class="card">
355
- <h2>Repo (all sessions)</h2>
356
- <div class="stat-grid">
357
- <span class="label">Checkpoints</span><span class="value" id="rp-count">0</span>
358
- <span class="label">Original (dropped)</span><span class="value" id="rp-in">0</span>
359
- <span class="label">Kept (summaries)</span><span class="value" id="rp-kept">0</span>
360
- <span class="label">Freed (dropped − kept)</span><span class="value" id="rp-freed">0</span>
361
- <span class="label">Sessions</span><span class="value" id="rp-sessions">0</span>
362
- <span class="label">Collapsed</span><span class="value" id="rp-collapsed">0</span>
363
- <span class="label">Storage Dedup</span><span class="value" id="rp-sdedup">0%</span>
364
- </div>
365
- <div class="meter-track" style="margin-top:10px"><div class="meter-fill" id="rp-compress-bar" style="width:0%"></div></div>
366
- <div class="meter-sub" id="rp-compress-sub">waiting for compaction…</div>
367
- </div>
368
- <div class="card safe">
369
- <h2>🛡 Data Safety</h2>
370
- <div class="stat-grid">
371
- <span class="label">Regions Retained</span><span class="value" id="ig-retained">0</span>
372
- <span class="label">Compressed-Original</span><span class="value" id="ig-bytes">0 B</span>
373
- <span class="label">Dedup Duplicates</span><span class="value" id="ig-dupes">0</span>
374
- <span class="label">Permanently Deleted</span><span class="value ok" id="ig-deleted">0 B</span>
375
- </div>
376
- <p class="safe-note">Every compacted region is kept verbatim (compressed). "Drop" = removed from the live window only. We never delete your data.</p>
377
- </div>
378
- <div class="card">
379
- <h2>Configuration</h2>
380
- <div class="conf-grid">
381
- <span class="label" title="Live pressure band — climbs low→mega as context fills the window.">Tier (live)</span><span class="value" id="cf-tier">${tierName}</span>
382
- <span class="label" title="The env-resolved base compaction preset (low/medium/high/ultra/mega) that set the token threshold.">Preset</span><span class="value" id="cf-preset">—</span>
383
- <span class="label" title="Live pressure = currentTokens / threshold — % of the model context window (threshold fires at the tier's % of window).">Pressure</span><span class="value" id="cf-pressure">—</span>
384
- <span class="label" title="Compaction threshold = tierPct × model context window — mega-compact trims BELOW pi's native ~80% auto-compact for any model size.">Threshold</span><span class="value" id="cf-threshold">—</span>
385
- <span class="label" title="Fast-gate arming floor — the live trim arms once context passes this % of the window.">Fast Gate</span><span class="value" id="cf-gate">—</span>
386
- <span class="label">Auto</span><span class="value" id="cf-auto">—</span>
387
- <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
388
- </div>
389
- </div>
390
- <div class="card cost">
391
- <h2>💰 Model &amp; Cost Savings</h2>
392
- <div class="cost-usd" id="cost-usd">≈ $0.00 saved</div>
393
- <div class="cost-sub" id="cost-windows">0 context-windows extended</div>
394
- <div class="stat-grid" style="margin-top:12px">
395
- <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>
396
- <span class="label" title="The provider serving the model.">Provider</span><span class="value" id="md-provider">—</span>
397
- <span class="label" title="USD per input token, from the model's pricing.">Input Rate</span><span class="value" id="md-input">—</span>
398
- <span class="label" title="USD per output token, from the model's pricing.">Output Rate</span><span class="value" id="md-output">—</span>
399
- </div>
400
- </div>
401
- <div class="card">
402
- <h2>Crew / Agents</h2>
403
- <div class="stat-grid">
404
- <span class="label">Active Agents</span><span class="value" id="cr-agents">0</span>
405
- <span class="label">Current Turn</span><span class="value" id="cr-turn">0</span>
406
- <span class="label">Status</span><span class="value" id="cr-status">idle</span>
407
- </div>
408
- </div>
409
- <div class="card legend">
410
- <h2>What these numbers mean</h2>
411
- <ul class="legend-list">
412
- <li><b>Original (dropped)</b> — everything compacted away (including duplicates caught by dedup). The "in."</li>
413
- <li><b>Kept (summaries)</b> — compact summaries still held as "memory" (the "out").</li>
414
- <li><b>Freed</b> = dropped − kept — tokens saved so far (higher = better).</li>
415
- <li><b>Compression %</b> — Freed ÷ Dropped — the headline efficiency number. Higher = more space reclaimed.</li>
416
- <li><b>Storage dedup %</b> — how often new content matched something already saved, so no duplicate copy was written.</li>
417
- <li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
418
- </ul>
419
- <p class="legend-note">Hover any label above for a quick explanation.</p>
420
- </div>
421
- <div class="card">
422
- <h2>💾 Cache Hits &amp; 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>
441
- </div>
442
-
443
- <div class="events">
444
- <h2>Event Stream</h2>
445
- <div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
446
- </div>
447
-
448
- <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
449
- <table class="repos">
450
- <thead>
451
- <tr>
452
- <th>Repo</th><th>Model</th>
453
- <th style="text-align:right">Checkpoints</th>
454
- <th style="text-align:right">Tokens Saved</th>
455
- <th style="text-align:right">Retained</th>
456
- <th style="text-align:right">Last Compacted</th>
457
- </tr>
458
- </thead>
459
- <tbody id="cur-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
460
- </table>
461
- <div class="updated" id="cur-updated"></div>
462
-
463
- <div class="updated" id="updated"></div>
464
- </div><!-- /panel-current -->
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 &amp; 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
-
488
- <!-- Per-repo detail modal -->
489
- <div class="repo-detail" id="repo-detail">
490
- <div class="repo-detail-box">
491
- <h2><span id="rd-name">Repo</span><button class="repo-close" id="rd-close" title="Close">×</button></h2>
492
- <div class="repo-path" id="rd-path"></div>
493
- <div class="stat-grid">
494
- <span class="label">Model</span><span class="value" id="rd-model">—</span>
495
- <span class="label">Checkpoints</span><span class="value" id="rd-cp">0</span>
496
- <span class="label">Tokens Saved</span><span class="value" id="rd-saved">0</span>
497
- <span class="label">Compressed-Original</span><span class="value" id="rd-bytes">0 B</span>
498
- <span class="label">Last Compacted</span><span class="value" id="rd-when">—</span>
499
- <span class="label">Provider</span><span class="value" id="rd-provider">—</span>
500
- </div>
501
- </div>
502
- </div>
503
-
504
- <!-- All repos (machine-wide registry from index.sqlite) -->
505
- <div class="tab-panel" id="panel-all">
506
- <table class="repos">
507
- <thead>
508
- <tr>
509
- <th>Repo</th><th>Model</th>
510
- <th style="text-align:right">Checkpoints</th>
511
- <th style="text-align:right">Tokens Saved</th>
512
- <th style="text-align:right">Retained</th>
513
- <th style="text-align:right">Last Compacted</th>
514
- </tr>
515
- </thead>
516
- <tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
517
- </table>
518
- <div class="updated" id="all-updated"></div>
519
- </div>
520
-
521
- <!-- Summary (aggregate across all repos) -->
522
- <div class="tab-panel" id="panel-summary">
523
- <div class="summary-grid">
524
- <div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
525
- <div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
526
- <div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
527
- <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
528
- </div>
529
-
530
- <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
531
- <p class="legend-note" style="margin-bottom:10px">How much context &amp; cost mega-compact has reclaimed, grouped by the model you were running. Compression ratio reflects workload/content, not model quality.</p>
532
- <table class="repos">
533
- <thead>
534
- <tr>
535
- <th>Model</th><th>Provider</th>
536
- <th style="text-align:right" title="Tokens dropped from context by compaction (the input reclaimed)">Tokens In</th>
537
- <th style="text-align:right" title="Tokens kept as compacted summaries still in context (the output retained)">Tokens Out</th>
538
- <th style="text-align:right">Freed</th>
539
- <th style="text-align:right" title="Model context window (max input tokens the model accepts)">Ctx Window</th>
540
- <th style="text-align:right" title="Model max output tokens per turn">Max Out</th>
541
- <th style="text-align:right" title="Reasoning-capable model">Reas.</th>
542
- <th style="text-align:right" title="Distinct sessions with at least one checkpoint">Sessions</th>
543
- <th style="text-align:right">Checkpoints</th>
544
- <th style="text-align:right" title="USD per input token">In $/tok</th>
545
- <th style="text-align:right" title="USD per output token">Out $/tok</th>
546
- <th style="text-align:right">$ Saved</th>
547
- <th style="text-align:right">Last Used</th>
548
- </tr>
549
- </thead>
550
- <tbody id="bm-rows"><tr><td colspan="14" class="repo-none">loading…</td></tr></tbody>
551
- </table>
552
- <p class="legend-note" style="margin-top:8px">Tokens In = Σ original region tokens dropped by compaction. Tokens Out = Σ compacted summary tokens still retained in context. Freed = Tokens In − Tokens Out (net context reclaimed). Ctx Window / Max Out / Reas. come from the latest captured model snapshot for each repo.</p>
553
-
554
- <div class="updated" id="sm-updated"></div>
555
- </div>
556
-
557
- <script>
558
- (function() {
559
- var evBox = document.getElementById('events');
560
- var evBuffer = [];
561
- var MAX_EV = 50;
562
- var offlineBanner = document.getElementById('offline-banner');
563
-
564
- function bullet(el, on, na) {
565
- el.className = 'bullet ' + (na ? 'bullet-na' : on ? 'bullet-on' : 'bullet-off');
566
- }
567
-
568
- function renderSnapshot(d) {
569
- if (!d || !d.updatedAt) { offlineBanner.style.display = 'block'; return; }
570
- offlineBanner.style.display = 'none';
571
-
572
- var pct = d.context.percent || 0;
573
- document.getElementById('ctx-pct').textContent = pct + '%';
574
- var bar = document.getElementById('ctx-bar');
575
- bar.style.width = Math.max(pct, 1) + '%';
576
- bar.className = 'meter-fill ' + (pct >= 90 ? 'meter-red' : pct >= 70 ? 'meter-yellow' : 'meter-green');
577
- var tok = d.context.tokens != null ? d.context.tokens.toLocaleString() : '?';
578
- var win = d.context.contextWindow ? d.context.contextWindow.toLocaleString() : '?';
579
- document.getElementById('ctx-sub').textContent = tok + ' / ' + win + ' tokens';
580
-
581
- bullet(document.getElementById('tr-armed'), d.trigger.armed, false);
582
- bullet(document.getElementById('tr-ready'), d.trigger.ready, !d.trigger.armed);
583
- var state = d.trigger.ready ? 'THRESHOLD EXCEEDED — compacting next event' :
584
- d.trigger.armed ? 'past fast gate — monitoring token count' : 'idle — below fast gate';
585
- document.getElementById('tr-state').textContent = state;
586
-
587
- // ---- Vector Store — reconciled token accounting (same formula as widget) -
588
- document.getElementById('st-count').textContent = d.store.checkpointCount;
589
- // Compression block from the snapshot (Freed = In − Out, single formula).
590
- var c = d.compression || {};
591
- var sess = c.session || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
592
- var cRepo = c.repo || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
593
- document.getElementById('st-in').textContent = sess.tokensIn.toLocaleString();
594
- document.getElementById('st-kept').textContent = sess.tokensOut.toLocaleString();
595
- document.getElementById('st-freed').textContent = sess.tokensFreed.toLocaleString();
596
- var sp = sess.compressionPct || 0;
597
- document.getElementById('st-compress-bar').style.width = Math.max(sp * 100, 0.5) + '%';
598
- document.getElementById('st-compress-bar').className = 'meter-fill ' + (sp >= 0.9 ? 'meter-green' : sp >= 0.6 ? 'meter-yellow' : 'meter-red');
599
- document.getElementById('st-compress-sub').textContent = (sp * 100 >= 10 ? Math.round(sp * 100) : (sp * 100).toFixed(1)) + '% tokens saved · dedup: ' + (sess.dedupPct * 100 >= 10 ? Math.round(sess.dedupPct * 100) : (sess.dedupPct * 100).toFixed(1)) + '%';
600
- // ------
601
- document.getElementById('st-injected').textContent = d.store.injectedCount;
602
- document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
603
- var sdr = d.store.storageDedupRate || 0;
604
- document.getElementById('st-sdedup').textContent = (sdr * 100 >= 10 ? Math.round(sdr * 100) : (sdr * 100).toFixed(1)) + '%';
605
- document.getElementById('st-collapsed').textContent = d.store.dedupCollapsed || 0;
606
- document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
607
-
608
- // ---- Repo (all sessions) — same compression fields, repo scope ----------
609
- document.getElementById('rp-count').textContent = (d.repo && d.repo.checkpointCount || 0).toLocaleString();
610
- document.getElementById('rp-in').textContent = cRepo.tokensIn.toLocaleString();
611
- document.getElementById('rp-kept').textContent = cRepo.tokensOut.toLocaleString();
612
- document.getElementById('rp-freed').textContent = cRepo.tokensFreed.toLocaleString();
613
- document.getElementById('rp-sessions').textContent = (d.repo && d.repo.sessionCount || 0).toLocaleString();
614
- document.getElementById('rp-collapsed').textContent = (d.repo && d.repo.dedupCollapsed || 0).toLocaleString();
615
- var rdr = d.repo && d.repo.storageDedupRate || 0;
616
- document.getElementById('rp-sdedup').textContent = (rdr * 100 >= 10 ? Math.round(rdr * 100) : (rdr * 100).toFixed(1)) + '%';
617
- var rp = cRepo.compressionPct || 0;
618
- document.getElementById('rp-compress-bar').style.width = Math.max(rp * 100, 0.5) + '%';
619
- document.getElementById('rp-compress-bar').className = 'meter-fill ' + (rp >= 0.9 ? 'meter-green' : rp >= 0.6 ? 'meter-yellow' : 'meter-red');
620
- document.getElementById('rp-compress-sub').textContent = (rp * 100 >= 10 ? Math.round(rp * 100) : (rp * 100).toFixed(1)) + '% tokens saved · dedup: ' + (cRepo.dedupPct * 100 >= 10 ? Math.round(cRepo.dedupPct * 100) : (cRepo.dedupPct * 100).toFixed(1)) + '%';
621
-
622
- // Data-safety invariant (Phase 0 — trust foundation).
623
- var ig = d.integrity || { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 };
624
- function fmtBytes(b) {
625
- b = b || 0;
626
- if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
627
- if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
628
- return b + ' B';
629
- }
630
- document.getElementById('ig-retained').textContent = (ig.regionsRetained || 0).toLocaleString();
631
- document.getElementById('ig-bytes').textContent = fmtBytes(ig.compressedOriginalBytes);
632
- document.getElementById('ig-dupes').textContent = (ig.duplicatesCollapsed || 0).toLocaleString();
633
- document.getElementById('ig-deleted').textContent = fmtBytes(ig.bytesPermanentlyDeleted);
634
-
635
- // Crew / agents (live sub-agent activity + turn).
636
- var crew = d.crew || { activeAgents: 0, currentTurn: 0 };
637
- document.getElementById('cr-agents').textContent = crew.activeAgents || 0;
638
- document.getElementById('cr-turn').textContent = crew.currentTurn || 0;
639
- document.getElementById('cr-status').textContent = (crew.activeAgents > 0)
640
- ? ('▶ ' + crew.activeAgents + ' running') : 'idle';
641
-
642
- // S24: headline tier is the LIVE pressure band; the config card shows the
643
- // env preset + live pressure ratio so the user sees the system react.
644
- document.getElementById('hdr-tier').textContent = d.tier;
645
- document.getElementById('cf-tier').textContent = d.tier + ' (live)';
646
- document.getElementById('cf-preset').textContent = d.presetTier;
647
- document.getElementById('cf-pressure').textContent = Math.round((d.pressure || 0) * 100) + '%';
648
- // (b) Threshold: show the effective token threshold AND the % of the model
649
- // context window it represents (percentage-based tiers). d.config.tierPct
650
- // is present on the live snapshot written by the runtime (Phase-1/2a).
651
- var cfgPct = d.config.tierPct;
652
- var cw = d.context.contextWindow || 0;
653
- var thresholdTxt = d.config.thresholdTokens.toLocaleString();
654
- if (cfgPct != null && cw > 0) {
655
- thresholdTxt += ' (' + Math.round(cfgPct * 100) + '% of ' + cw.toLocaleString() + ')';
656
- }
657
- document.getElementById('cf-threshold').textContent = thresholdTxt;
658
- // (c) Fast Gate: arming floor — live trim arms once context passes this %.
659
- document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
660
- document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
661
- document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
662
-
663
- // --- Active model + cost savings (same calc as /mega-status) ---------------
664
- var model = d.model;
665
- document.getElementById('hdr-model').textContent = model && model.name ? model.name : '—';
666
- document.getElementById('md-name').textContent = model && model.name ? model.name : '—';
667
- document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
668
- document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
669
- document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
670
- var repoSaved = cRepo.tokensFreed || 0;
671
- if (model && model.inputRate && repoSaved > 0) {
672
- var usd = (repoSaved * model.inputRate);
673
- var win = d.context.contextWindow || 0;
674
- var windows = win > 0 ? (repoSaved / win).toFixed(1) : '0';
675
- document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
676
- document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
677
- } else {
678
- document.getElementById('cost-usd').textContent = '≈ $0.00 saved';
679
- document.getElementById('cost-windows').textContent = '0 context-windows extended';
680
- }
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
-
697
- document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
698
- }
699
-
700
- function sanitize(s) {
701
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
702
- }
703
-
704
- function renderEvent(ev) {
705
- evBuffer.unshift(ev);
706
- if (evBuffer.length > MAX_EV) evBuffer.length = MAX_EV;
707
- evBox.innerHTML = evBuffer.map(function(e) {
708
- var t = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
709
- var detail = '';
710
- if (e.data) {
711
- if (e.data.checkpointId) detail = sanitize(e.data.checkpointId);
712
- else if (e.data.query) detail = sanitize(e.data.query.slice(0, 80));
713
- if (e.data.tokenEstimate != null) detail += ' ' + e.data.tokenEstimate + ' tok';
714
- if (e.data.deduped) detail += ' (deduped)';
715
- if (e.data.injected != null) detail = 'injected: ' + e.data.injected + (e.data.empty ? ' (empty)' : '');
716
- }
717
- return '<div class="ev">' +
718
- '<span class="ev-time">' + t + '</span>' +
719
- '<span class="ev-type ev-type-' + sanitize(e.type) + '">' + sanitize(e.type) + '</span>' +
720
- '<span class="ev-detail">' + detail + '</span></div>';
721
- }).join('');
722
- }
723
-
724
- // Poll snapshot every 2s
725
- function pollSnapshot() {
726
- fetch('/api/snapshot').then(function(r) { return r.json(); }).then(renderSnapshot).catch(function() {});
727
- }
728
- pollSnapshot();
729
- setInterval(pollSnapshot, 2000);
730
-
731
- // SSE for events
732
- function connectSSE() {
733
- var es = new EventSource('/api/events');
734
- es.onmessage = function(msg) {
735
- try { renderEvent(JSON.parse(msg.data)); } catch(e) {}
736
- };
737
- es.onerror = function() {
738
- es.close();
739
- setTimeout(connectSSE, 3000);
740
- };
741
- }
742
- connectSSE();
743
-
744
- // --- Multi-repo (index.sqlite via /api/index) ---------------------------
745
- function fmtBytesTop(b) {
746
- b = b || 0;
747
- if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
748
- if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
749
- return b + ' B';
750
- }
751
- function renderIndex(d) {
752
- d = d || { updatedAt: null, summary: null, repos: [] };
753
- var repos = d.repos || [];
754
- var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
755
- document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
756
- document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
757
- document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
758
- document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
759
-
760
- // Shared clickable-row renderer for both the in-current table and the
761
- // All-repos tab — each row opens the per-repo detail modal.
762
- function rowsHtml() {
763
- if (!repos.length) return '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
764
- return repos.map(function(r) {
765
- var model = r.modelName
766
- ? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
767
- : '<span class="repo-none">—</span>';
768
- var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
769
- return '<tr class="repo-link" data-repo="' + sanitize(r.repoRoot) + '">' +
770
- '<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
771
- '<td>' + model + '</td>' +
772
- '<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
773
- '<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
774
- '<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
775
- '<td class="num">' + sanitize(when) + '</td>' +
776
- '</tr>';
777
- }).join('');
778
- }
779
- document.getElementById('cur-rows').innerHTML = rowsHtml();
780
- document.getElementById('all-rows').innerHTML = rowsHtml();
781
- bindRepoRows();
782
-
783
- var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
784
- document.getElementById('cur-updated').textContent = stamp;
785
- document.getElementById('all-updated').textContent = stamp;
786
- document.getElementById('sm-updated').textContent = stamp;
787
- renderByModel(repos);
788
- }
789
-
790
- // Savings-by-model aggregation for the Summary tab — groups the machine-
791
- // wide repo registry by (modelName || '(unknown)') so the user can see how
792
- // much context + cost mega-compact has reclaimed, broken down by which model
793
- // they were running. $ Saved = Σ(tokensSaved × inputRate) per model. Sorted
794
- // by tokens saved descending so the biggest-reclaim model wins the top row.
795
- function renderByModel(repos) {
796
- var rows = document.getElementById('bm-rows');
797
- if (!rows) return;
798
- if (!repos || !repos.length) {
799
- rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
800
- return;
801
- }
802
- var groups = {};
803
- for (var i = 0; i < repos.length; i++) {
804
- var r = repos[i];
805
- var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
806
- if (!groups[key]) groups[key] = {
807
- model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0,
808
- tokensSaved: 0, tokensIn: 0, tokensOut: 0, sessions: 0, usd: 0, lastAt: 0,
809
- inRates: [], outRates: [], ctxWindows: [], maxTokens: [], reasoning: null,
810
- };
811
- var g = groups[key];
812
- g.repos++;
813
- g.checkpoints += (r.checkpointCount || 0);
814
- g.tokensSaved += (r.tokensSaved || 0);
815
- g.tokensIn += (r.tokensDropped || 0);
816
- g.tokensOut += (r.tokensKept || 0);
817
- g.sessions += (r.sessions || 0);
818
- if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.inRates.push(r.inputRate); }
819
- if (r.outputRate) g.outRates.push(r.outputRate);
820
- if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
821
- if (r.maxTokens) g.maxTokens.push(r.maxTokens);
822
- if (r.reasoning != null) g.reasoning = r.reasoning;
823
- if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
824
- }
825
- var arr = [];
826
- for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
827
- arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
828
- // Helper: a set of numeric samples collapses to a single value when all
829
- // repos in the group agree, otherwise shows the range (min–max) so the
830
- // user can see mixed-config model groups at a glance.
831
- function collapseNum(samples) {
832
- if (!samples || !samples.length) return '—';
833
- var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
834
- return lo === hi ? lo.toLocaleString() : lo.toLocaleString() + '–' + hi.toLocaleString();
835
- }
836
- function collapseRate(samples) {
837
- if (!samples || !samples.length) return '—';
838
- var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
839
- var fmt = function(v) { return '$' + v.toFixed(6); };
840
- return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
841
- }
842
- rows.innerHTML = arr.map(function(g) {
843
- var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
844
- var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
845
- var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
846
- var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
847
- return '<tr>' +
848
- '<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
849
- '<td>' + sanitize(g.provider) + '</td>' +
850
- '<td class="num">' + (g.tokensIn || 0).toLocaleString() + '</td>' +
851
- '<td class="num">' + (g.tokensOut || 0).toLocaleString() + '</td>' +
852
- '<td class="num">' + freed.toLocaleString() + '</td>' +
853
- '<td class="num">' + collapseNum(g.ctxWindows) + '</td>' +
854
- '<td class="num">' + collapseNum(g.maxTokens) + '</td>' +
855
- '<td class="num">' + reas + '</td>' +
856
- '<td class="num">' + g.sessions.toLocaleString() + '</td>' +
857
- '<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
858
- '<td class="num">' + collapseRate(g.inRates) + '</td>' +
859
- '<td class="num">' + collapseRate(g.outRates) + '</td>' +
860
- '<td class="num">' + sanitize(usd) + '</td>' +
861
- '<td class="num">' + sanitize(when) + '</td>' +
862
- '</tr>';
863
- }).join('');
864
- }
865
-
866
- // Per-repo detail modal ---------------------------------------------------
867
- var detailEl = document.getElementById('repo-detail');
868
- var indexCache = { repos: [] };
869
- function openRepoDetail(root) {
870
- var r = null;
871
- for (var i = 0; i < indexCache.repos.length; i++) {
872
- if (indexCache.repos[i].repoRoot === root) { r = indexCache.repos[i]; break; }
873
- }
874
- if (!r) return;
875
- document.getElementById('rd-name').textContent = r.displayName || r.repoRoot;
876
- document.getElementById('rd-path').textContent = r.repoRoot;
877
- document.getElementById('rd-model').textContent = r.modelName || '—';
878
- document.getElementById('rd-provider').textContent = r.providerName || (r.provider || '—');
879
- document.getElementById('rd-cp').textContent = (r.checkpointCount || 0).toLocaleString();
880
- document.getElementById('rd-saved').textContent = (r.tokensSaved || 0).toLocaleString();
881
- document.getElementById('rd-bytes').textContent = fmtBytesTop(r.compressedOriginalBytes);
882
- document.getElementById('rd-when').textContent = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
883
- detailEl.classList.add('open');
884
- }
885
- document.getElementById('rd-close').addEventListener('click', function() { detailEl.classList.remove('open'); });
886
- detailEl.addEventListener('click', function(e) { if (e.target === detailEl) detailEl.classList.remove('open'); });
887
- function bindRepoRows() {
888
- var rows = document.querySelectorAll('.repo-link');
889
- for (var i = 0; i < rows.length; i++) {
890
- rows[i].addEventListener('click', function() { openRepoDetail(this.getAttribute('data-repo')); });
891
- }
892
- }
893
- function pollIndex() {
894
- fetch('/api/index').then(function(r) { return r.json(); }).then(function(d) {
895
- indexCache = d && d.repos ? d : indexCache;
896
- renderIndex(d);
897
- }).catch(function() {});
898
- }
899
- pollIndex();
900
- setInterval(pollIndex, 5000);
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
-
944
- // --- Tab switching ------------------------------------------------------
945
- var tabs = document.querySelectorAll('.tab');
946
- var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary' };
947
- for (var i = 0; i < tabs.length; i++) {
948
- tabs[i].addEventListener('click', function() {
949
- var name = this.getAttribute('data-tab');
950
- for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
951
- this.classList.add('active');
952
- for (var k in panels) {
953
- if (Object.prototype.hasOwnProperty.call(panels, k)) {
954
- var el = document.getElementById(panels[k]);
955
- if (el) el.classList.toggle('active', k === name);
956
- }
957
- }
958
- if (name === 'all' || name === 'summary') pollIndex();
959
- if (name === 'active') pollServers();
960
- });
961
- }
962
- })();
963
- </script>
964
- </body>
965
- </html>`;
966
- }
967
- // ---------------------------------------------------------------------------
968
- // Server
969
- // ---------------------------------------------------------------------------
970
- export async function launchDashboardServer(stateDir) {
971
- // Our own package version — exposed at /api/version so the launcher can
972
- // detect a stale server (started by an older build) and replace it on
973
- // upgrade instead of reuse it.
974
- let SERVER_VERSION = "0.0.0";
975
- try {
976
- // dashboard-server.js lives at <pkg>/dist/extensions/, so package.json is
977
- // two levels up. Guard each candidate so a dev-checkout layout still works.
978
- const here = dirname(fileURLToPath(import.meta.url));
979
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
980
- for (const p of candidates) {
981
- if (!existsSync(p))
982
- continue;
983
- const pkg = JSON.parse(readFileSync(p, "utf-8"));
984
- if (pkg.version) {
985
- SERVER_VERSION = pkg.version;
986
- dashboardServerVersion = pkg.version;
987
- break;
988
- }
989
- }
990
- }
991
- catch { /* non-fatal */ }
992
- // Lazy-loaded via require so the dashboard stays cheap to boot and we don't
993
- // need a top-level await in the handler.
994
- const driftReq = createRequire(import.meta.url);
995
- const detectCrossRepoDrift = (idxDir) => driftReq("../src/driftDetection.js")
996
- .detectCrossRepoDrift(idxDir);
997
- const portFile = join(stateDir, "port.pid");
998
- const snapshotPath = join(stateDir, "dashboard.json");
999
- const eventsPath = join(stateDir, "events.log");
1000
- LOG_PATH = join(stateDir, "dashboard.log");
1001
- log("launch invoked", { stateDir });
1002
- // ── Existing server? ───────────────────────────────────────────────────────
1003
- // A stale port.pid pointing at a dead/competing process is the classic cause
1004
- // of "dashboard failed to start" — we return a port that is NOT actually
1005
- // serving. Probe for a live server on that port first; only reuse the marker
1006
- // when something real answers /api/version. Otherwise drop it and start fresh.
1007
- if (existsSync(portFile)) {
1008
- try {
1009
- const info = JSON.parse(readFileSync(portFile, "utf-8"));
1010
- if (info && info.port) {
1011
- let live = false;
1012
- try {
1013
- const probe = await fetch(`http://localhost:${info.port}/api/version`, { signal: AbortSignal.timeout(800) });
1014
- live = probe.ok;
1015
- }
1016
- catch {
1017
- live = false;
1018
- }
1019
- if (live) {
1020
- log("reusing live server from port.pid", { port: info.port });
1021
- return { port: info.port, url: `http://localhost:${info.port}` };
1022
- }
1023
- log("port.pid present but no live server — treating as stale", { port: info.port });
1024
- }
1025
- }
1026
- catch {
1027
- log("port.pid unparseable — treating as stale");
1028
- }
1029
- // stale file, remove so the fresh bind does not collide with a lingering
1030
- // process that still holds the port
1031
- try {
1032
- unlinkSync(portFile);
1033
- }
1034
- catch { /* ignore */ }
1035
- }
1036
- // ── New server ────────────────────────────────────────────────────────────
1037
- mkdirSync(stateDir, { recursive: true });
1038
- let eventOffset = 0;
1039
- // Overlay the live current-repo snapshot (snapshot.json, rewritten every
1040
- // context event) onto its registry row so the All-repos / Summary views stay
1041
- // in sync with the live menu bar + Current-repo card in real time. The
1042
- // registry (index.sqlite) is only written on repo-switch (bindRepo), so
1043
- // without this the current repo's row freezes between switches. Read-only —
1044
- // no extra writes to index.sqlite. Matched by stateDir, which equals the
1045
- // value this server was launched with (runtime.currentStateDir).
1046
- function overlayCurrentRepo(idx) {
1047
- if (!idx || !idx.repos.length)
1048
- return;
1049
- let snap = null;
1050
- try {
1051
- snap = readSnapshot(snapshotPath);
1052
- }
1053
- catch {
1054
- return;
1055
- }
1056
- if (!snap || !snap.repo)
1057
- return;
1058
- const cur = idx.repos.find((r) => r.stateDir === stateDir);
1059
- if (!cur)
1060
- return;
1061
- const prevSaved = cur.tokensSaved;
1062
- const prevCp = cur.checkpointCount;
1063
- const prevBytes = cur.compressedOriginalBytes;
1064
- const comp = snap.compression?.repo;
1065
- const liveSaved = comp ? comp.tokensFreed : (snap.repo.tokensSaved ?? prevSaved);
1066
- const liveCp = snap.repo.checkpointCount ?? prevCp;
1067
- const liveBytes = snap.integrity?.compressedOriginalBytes ?? prevBytes;
1068
- cur.tokensSaved = liveSaved;
1069
- cur.checkpointCount = liveCp;
1070
- cur.compressedOriginalBytes = liveBytes;
1071
- if (idx.summary) {
1072
- idx.summary.totalTokensSaved += liveSaved - prevSaved;
1073
- idx.summary.totalCheckpoints += liveCp - prevCp;
1074
- idx.summary.totalCompressedOriginalBytes += liveBytes - prevBytes;
1075
- }
1076
- idx.updatedAt = snap.updatedAt ?? idx.updatedAt;
1077
- }
1078
- const server = createServer((req, res) => {
1079
- // guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard localhost server (loopback-only) — CORS open for local browser access
1080
- // CORS for local access
1081
- res.setHeader("Access-Control-Allow-Origin", "*");
1082
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
1083
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1084
- if (req.method === "OPTIONS") {
1085
- res.writeHead(204);
1086
- res.end();
1087
- return;
1088
- }
1089
- if (req.url === "/" || req.url === "/index.html") {
1090
- const tier = readSnapshot(snapshotPath).tier;
1091
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1092
- res.end(dashboardHtml(tier));
1093
- return;
1094
- }
1095
- if (req.url === "/api/snapshot") {
1096
- const snap = readSnapshot(snapshotPath);
1097
- res.writeHead(200, { "Content-Type": "application/json" });
1098
- res.end(JSON.stringify(snap));
1099
- return;
1100
- }
1101
- // Server version — lets the /dashboard launcher detect a stale server from
1102
- // an older build and replace it on upgrade rather than reuse it.
1103
- if (req.url === "/api/version") {
1104
- res.writeHead(200, { "Content-Type": "application/json" });
1105
- res.end(JSON.stringify({ version: SERVER_VERSION }));
1106
- return;
1107
- }
1108
- // Multi-repo aggregate (Phase 5b): the machine-wide repo registry read
1109
- // directly from SQLite (index.sqlite). Lets one dashboard show every repo's
1110
- // checkpoints, tokens saved, and active model. Read-only.
1111
- if (req.url === "/api/index") {
1112
- const idx = readIndex();
1113
- if (idx)
1114
- overlayCurrentRepo(idx);
1115
- res.writeHead(200, { "Content-Type": "application/json" });
1116
- res.end(JSON.stringify(idx ?? { updatedAt: null, summary: null, repos: [] }));
1117
- return;
1118
- }
1119
- // /api/repos — registry list. Optional `?active=24h` filters to repos
1120
- // seen within the last N hours (default: all). The dashboard uses this to
1121
- // drive its "active vs archived" badge without refetching /api/index.
1122
- if (req.url?.startsWith("/api/repos")) {
1123
- const url = new URL(req.url, "http://x");
1124
- const activeParam = url.searchParams.get("active");
1125
- const idx = readIndex();
1126
- if (idx)
1127
- overlayCurrentRepo(idx);
1128
- let repos = idx?.repos ?? [];
1129
- if (activeParam) {
1130
- const m = /^(\d+)h$/.exec(activeParam);
1131
- if (m) {
1132
- const cutoffSec = Math.floor(Date.now() / 1000) - Number(m[1]) * 3600;
1133
- repos = repos.filter((r) => (r.lastSeen ?? 0) >= cutoffSec);
1134
- }
1135
- }
1136
- res.writeHead(200, { "Content-Type": "application/json" });
1137
- res.end(JSON.stringify({ updatedAt: idx?.updatedAt ?? null, repos, count: repos.length }));
1138
- return;
1139
- }
1140
- // /api/summary — header tiles without the full repo list (keeps payload
1141
- // small for embed scenarios). activeRepos mirrors the /api/repos?active=24h
1142
- // count so the dashboard can render the active badge alongside totals.
1143
- if (req.url?.startsWith("/api/summary")) {
1144
- const idx = readIndex();
1145
- if (idx)
1146
- overlayCurrentRepo(idx);
1147
- const repos = idx?.repos ?? [];
1148
- const cutoffSec = Math.floor(Date.now() / 1000) - 24 * 3600;
1149
- const activeRepos = repos.filter((r) => (r.lastSeen ?? 0) >= cutoffSec).length;
1150
- res.writeHead(200, { "Content-Type": "application/json" });
1151
- res.end(JSON.stringify({
1152
- updatedAt: idx?.updatedAt ?? null,
1153
- summary: idx?.summary ?? null,
1154
- activeRepos,
1155
- totalRepos: repos.length,
1156
- }));
1157
- return;
1158
- }
1159
- // /api/drift — R4: cross-repo drift report over repo_registry. Flags stale
1160
- // repos (>30d idle), compaction lag (active but >24h since last
1161
- // compaction), and recent model churn. Read-only.
1162
- if (req.url?.startsWith("/api/drift")) {
1163
- const report = detectCrossRepoDrift(getIndexDir());
1164
- res.writeHead(200, { "Content-Type": "application/json" });
1165
- res.end(JSON.stringify(report));
1166
- return;
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
- }
1199
- if (req.url === "/api/events") {
1200
- res.writeHead(200, {
1201
- "Content-Type": "text/event-stream",
1202
- "Cache-Control": "no-cache",
1203
- "Connection": "keep-alive",
1204
- });
1205
- // Drain existing events so the client starts with history
1206
- const { data: existing, offset: initialOffset } = readFrom(eventsPath, 0);
1207
- eventOffset = initialOffset;
1208
- const lines = existing.split("\n").filter((l) => l.trim());
1209
- for (const line of lines) {
1210
- res.write(`data: ${line}\n\n`);
1211
- }
1212
- // Tail new events via fs.watch (coalesced with 100ms debounce)
1213
- let watchTimer = null;
1214
- const onWatch = () => {
1215
- if (watchTimer)
1216
- return;
1217
- watchTimer = setTimeout(() => {
1218
- watchTimer = null;
1219
- const { data, offset } = readFrom(eventsPath, eventOffset);
1220
- eventOffset = offset;
1221
- const newLines = data.split("\n").filter((l) => l.trim());
1222
- for (const line of newLines) {
1223
- res.write(`data: ${line}\n\n`);
1224
- }
1225
- }, 100);
1226
- };
1227
- // Set up file watching: if file exists, watch it directly;
1228
- // otherwise poll for creation every 1s then switch to fs.watch.
1229
- let watcher = null;
1230
- let pollInterval = null;
1231
- function startFileWatch() {
1232
- try {
1233
- watcher = watch(eventsPath, onWatch);
1234
- }
1235
- catch { /* give up */ }
1236
- }
1237
- if (existsSync(eventsPath)) {
1238
- startFileWatch();
1239
- }
1240
- else {
1241
- pollInterval = setInterval(() => {
1242
- if (existsSync(eventsPath)) {
1243
- if (pollInterval) {
1244
- clearInterval(pollInterval);
1245
- pollInterval = null;
1246
- }
1247
- startFileWatch();
1248
- }
1249
- }, 1000);
1250
- }
1251
- req.on("close", () => {
1252
- if (watchTimer)
1253
- clearTimeout(watchTimer);
1254
- if (pollInterval)
1255
- clearInterval(pollInterval);
1256
- watcher?.close();
1257
- });
1258
- return;
1259
- }
1260
- // Fallback — serve the dashboard
1261
- const tier = readSnapshot(snapshotPath).tier;
1262
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1263
- res.end(dashboardHtml(tier));
1264
- });
1265
- // Bind base + range are env-configurable so tests can use a private,
1266
- // non-colliding range (parallel runs / leftover servers from killed runs
1267
- // would otherwise EADDRINUSE on the machine-global 9320 range). Default
1268
- // MEGACOMPACT_DASHBOARD_PORT=9320 (10-port range 9320–9329) preserves the
1269
- // production behavior.
1270
- const TARGET_PORT = Number(process.env.MEGACOMPACT_DASHBOARD_PORT ?? "9320");
1271
- const PORT_RANGE = 10; // TARGET_PORT..TARGET_PORT+9
1272
- return new Promise((resolve, reject) => {
1273
- function tryPort(port) {
1274
- server.once("error", (err) => {
1275
- if (err.code === "EADDRINUSE" && port < TARGET_PORT + PORT_RANGE - 1) {
1276
- log("port in use, trying next", { port });
1277
- tryPort(port + 1);
1278
- }
1279
- else {
1280
- log("listen failed", { port, code: err.code, message: err.message });
1281
- reject(err);
1282
- }
1283
- });
1284
- server.listen(port, "127.0.0.1", () => {
1285
- const url = `http://localhost:${port}`;
1286
- log("server running", { url });
1287
- // eslint-disable-next-line no-console
1288
- console.log(`[mega-compact] dashboard server running: ${url}`);
1289
- // Write port.pid
1290
- try {
1291
- writeFileSync(portFile, JSON.stringify({ port, pid: process.pid }));
1292
- }
1293
- catch (e) {
1294
- log("could not write port.pid", { error: String(e) });
1295
- }
1296
- // Graceful cleanup
1297
- const cleanup = () => {
1298
- try {
1299
- unlinkSync(portFile);
1300
- }
1301
- catch { /* already gone */ }
1302
- server.close();
1303
- process.exit(0);
1304
- };
1305
- process.on("SIGTERM", cleanup);
1306
- process.on("SIGINT", cleanup);
1307
- resolve({ port, url });
1308
- });
1309
- }
1310
- tryPort(TARGET_PORT);
1311
- });
1312
- }
1313
- // ---------------------------------------------------------------------------
1314
- // CLI entry point — when run directly as `node dashboard-server.js <stateDir>`
1315
- // ---------------------------------------------------------------------------
1316
- if (process.argv[1] && process.argv[1].includes("dashboard-server")) {
1317
- const stateDir = process.argv[2];
1318
- if (!stateDir) {
1319
- console.error("Usage: node dashboard-server.js <stateDir>");
1320
- process.exit(1);
1321
- }
1322
- launchDashboardServer(stateDir).catch((err) => {
1323
- console.error("[mega-compact] dashboard server failed:", err);
1324
- process.exit(1);
1325
- });
1326
- }
13
+ export * from "./dashboard-server/types.js";
14
+ export * from "./dashboard-server/state.js";
15
+ export * from "./dashboard-server/index-reader.js";
16
+ export * from "./dashboard-server/snapshot.js";
17
+ export * from "./dashboard-server/html.js";
18
+ export * from "./dashboard-server/server.js";