pi-mega-compact 0.4.0

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 (69) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +375 -0
  3. package/extensions/DASHBOARD.md +160 -0
  4. package/extensions/dashboard-server.test.ts +124 -0
  5. package/extensions/dashboard-server.ts +459 -0
  6. package/extensions/error-patterns.ts +175 -0
  7. package/extensions/mega-compact.test.ts +351 -0
  8. package/extensions/mega-compact.ts +846 -0
  9. package/extensions/openclaw-mega-compact.ts +370 -0
  10. package/package.json +61 -0
  11. package/src/adapt.ts +120 -0
  12. package/src/boundary.test.ts +61 -0
  13. package/src/boundary.ts +94 -0
  14. package/src/canary.ts +126 -0
  15. package/src/compact.test.ts +99 -0
  16. package/src/compact.ts +262 -0
  17. package/src/config/dedup.ts +120 -0
  18. package/src/config.ts +15 -0
  19. package/src/dedup/dedup.test.ts +46 -0
  20. package/src/dedup/digest.ts +40 -0
  21. package/src/dedup/l1-lsh.ts +67 -0
  22. package/src/dedup/l1-minhash.ts +90 -0
  23. package/src/dedup/l1-verify.ts +55 -0
  24. package/src/dedup/l1.test.ts +57 -0
  25. package/src/dedup/mmr.ts +54 -0
  26. package/src/dedup/normalize.ts +41 -0
  27. package/src/dedup/raptor/guardrails.ts +112 -0
  28. package/src/dedup/raptor/index.ts +118 -0
  29. package/src/dedup/raptor/kmeans.ts +156 -0
  30. package/src/dedup/raptor/raptor.test.ts +238 -0
  31. package/src/dedup/raptor/retrieval.ts +102 -0
  32. package/src/dedup/raptor/summarizer.ts +91 -0
  33. package/src/dedup/raptor/tree.ts +254 -0
  34. package/src/dedup/sprint12.test.ts +242 -0
  35. package/src/dedup/topk.ts +61 -0
  36. package/src/dedup-engine.test.ts +609 -0
  37. package/src/e2e.test.ts +843 -0
  38. package/src/embedder.ts +111 -0
  39. package/src/engine.test.ts +123 -0
  40. package/src/engine.ts +192 -0
  41. package/src/extractive.test.ts +156 -0
  42. package/src/extractive.ts +265 -0
  43. package/src/httpEmbedder.ts +154 -0
  44. package/src/log.test.ts +47 -0
  45. package/src/log.ts +60 -0
  46. package/src/monitoring.ts +171 -0
  47. package/src/ratio.bench.test.ts +1316 -0
  48. package/src/recall.integration.test.ts +96 -0
  49. package/src/recall.test.ts +59 -0
  50. package/src/recall.ts +100 -0
  51. package/src/sprint14.test.ts +245 -0
  52. package/src/store/backfill.ts +263 -0
  53. package/src/store/bloom.ts +122 -0
  54. package/src/store/compression.test.ts +83 -0
  55. package/src/store/compression.ts +203 -0
  56. package/src/store/integrity.ts +65 -0
  57. package/src/store/migrate.test.ts +158 -0
  58. package/src/store/migrate.ts +108 -0
  59. package/src/store/sprint10.test.ts +182 -0
  60. package/src/store/sqlite.ts +519 -0
  61. package/src/store.test.ts +169 -0
  62. package/src/store.ts +192 -0
  63. package/src/supersede.test.ts +42 -0
  64. package/src/supersede.ts +67 -0
  65. package/src/tokens.ts +35 -0
  66. package/src/types.test.ts +10 -0
  67. package/src/types.ts +49 -0
  68. package/src/vectorStore.test.ts +480 -0
  69. package/src/vectorStore.ts +544 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * dashboard-server.ts — lightweight local web dashboard for mega-compact.
3
+ *
4
+ * Zero npm dependencies. Uses only Node built-in modules (http, fs, path).
5
+ * Serves a single-page HTML dashboard, a JSON snapshot API, and an SSE
6
+ * endpoint that live-streams new events.log entries.
7
+ *
8
+ * Designed to be spawned as a detached child process from the pi extension
9
+ * and discovered by the /dashboard command via a port.pid file.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
15
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface Snapshot {
23
+ version: number;
24
+ updatedAt: string | null;
25
+ tier: string;
26
+ config: {
27
+ fastGatePct: number;
28
+ thresholdTokens: number;
29
+ anchorUserMessages: number;
30
+ preserveRecent: number;
31
+ auto: boolean;
32
+ autoInlineK: number;
33
+ };
34
+ session: {
35
+ id: string | null;
36
+ state: string | null;
37
+ persistedThisSession: boolean;
38
+ lastCheckpointId: string | null;
39
+ lastCompactedFrom: number;
40
+ };
41
+ context: {
42
+ tokens: number | null;
43
+ percent: number | null;
44
+ contextWindow: number;
45
+ };
46
+ trigger: {
47
+ armed: boolean;
48
+ ready: boolean;
49
+ currentTokens: number | null;
50
+ thresholdTokens: number;
51
+ fastGatePct: number;
52
+ };
53
+ store: {
54
+ checkpointCount: number;
55
+ totalTokenEstimate: number;
56
+ injectedCount: number;
57
+ dedupHitRate: number;
58
+ };
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function readSnapshot(snapshotPath: string) {
66
+ try {
67
+ const raw = readFileSync(snapshotPath, "utf-8");
68
+ return JSON.parse(raw) as Snapshot;
69
+ } catch {
70
+ return {
71
+ version: 1,
72
+ updatedAt: null,
73
+ tier: "unknown",
74
+ config: { fastGatePct: 80, thresholdTokens: 100_000, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
75
+ session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
76
+ context: { tokens: null, percent: null, contextWindow: 0 },
77
+ trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80 },
78
+ store: { checkpointCount: 0, totalTokenEstimate: 0, injectedCount: 0, dedupHitRate: 0 },
79
+ } as Snapshot;
80
+ }
81
+ }
82
+
83
+ function readFrom(path: string, charOffset: number): { data: string; offset: number } {
84
+ try {
85
+ const content = readFileSync(path, "utf-8");
86
+ if (content.length <= charOffset) return { data: "", offset: charOffset };
87
+ return { data: content.slice(charOffset), offset: content.length };
88
+ } catch {
89
+ return { data: "", offset: charOffset };
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // HTML template
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function dashboardHtml(tierName: string): string {
98
+ return `<!DOCTYPE html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="utf-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1">
103
+ <title>mega-compact dashboard</title>
104
+ <style>
105
+ * { margin: 0; padding: 0; box-sizing: border-box; }
106
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
107
+ h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
108
+ h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
109
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
110
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
111
+ .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
112
+ .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
113
+ .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
114
+ .meter-green { background: #238636; }
115
+ .meter-yellow { background: #d29922; }
116
+ .meter-red { background: #f85149; }
117
+ .meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
118
+ .meter-sub { font-size: 12px; color: #8b949e; }
119
+ .status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
120
+ .status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
121
+ .bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
122
+ .bullet-off { background: #484f58; }
123
+ .bullet-na { background: #d29922; }
124
+ .state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
125
+ .stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
126
+ .stat-grid .label { color: #8b949e; }
127
+ .stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
128
+ .conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
129
+ .conf-grid .label { color: #8b949e; }
130
+ .conf-grid .value { color: #f0f6fc; font-family: monospace; }
131
+ .events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
132
+ .events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
133
+ .events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
134
+ .ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
135
+ .ev:last-child { border-bottom: none; }
136
+ .ev-type { font-weight: 700; min-width: 70px; text-align: right; }
137
+ .ev-type-compact { color: #3fb950; }
138
+ .ev-type-recall { color: #a371f7; }
139
+ .ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
140
+ .ev-detail { color: #8b949e; flex: 1; }
141
+ .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
142
+ .empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
143
+ .offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
144
+ </style>
145
+ </head>
146
+ <body>
147
+
148
+ <div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
149
+
150
+ <h1><span>mega-compact</span><span class="tier">${tierName}</span></h1>
151
+
152
+ <div class="grid">
153
+ <div class="card">
154
+ <h2>Context Window</h2>
155
+ <div class="meter-label" id="ctx-pct">—</div>
156
+ <div class="meter-track"><div class="meter-fill" id="ctx-bar" style="width:0%"></div></div>
157
+ <div class="meter-sub" id="ctx-sub">waiting for data</div>
158
+ </div>
159
+ <div class="card">
160
+ <h2>Trigger Status</h2>
161
+ <div class="status-row"><div class="bullet" id="tr-armed"></div><span>Armed (context ≥ fast gate)</span></div>
162
+ <div class="status-row"><div class="bullet" id="tr-ready"></div><span>Ready (tokens ≥ threshold)</span></div>
163
+ <div class="state-text" id="tr-state">waiting</div>
164
+ </div>
165
+ <div class="card">
166
+ <h2>Vector Store</h2>
167
+ <div class="stat-grid">
168
+ <span class="label">Checkpoints</span><span class="value" id="st-count">0</span>
169
+ <span class="label">Total Tokens</span><span class="value" id="st-tokens">0</span>
170
+ <span class="label">Injected</span><span class="value" id="st-injected">0</span>
171
+ <span class="label">Dedup Rate</span><span class="value" id="st-dedup">0%</span>
172
+ <span class="label">Last ID</span><span class="value" id="st-lastid">—</span>
173
+ </div>
174
+ </div>
175
+ <div class="card">
176
+ <h2>Configuration</h2>
177
+ <div class="conf-grid">
178
+ <span class="label">Tier</span><span class="value" id="cf-tier">${tierName}</span>
179
+ <span class="label">Threshold</span><span class="value" id="cf-threshold">—</span>
180
+ <span class="label">Fast Gate</span><span class="value" id="cf-gate">—</span>
181
+ <span class="label">Auto</span><span class="value" id="cf-auto">—</span>
182
+ <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="events">
188
+ <h2>Event Stream</h2>
189
+ <div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
190
+ </div>
191
+
192
+ <div class="updated" id="updated"></div>
193
+
194
+ <script>
195
+ (function() {
196
+ var evBox = document.getElementById('events');
197
+ var evBuffer = [];
198
+ var MAX_EV = 50;
199
+ var offlineBanner = document.getElementById('offline-banner');
200
+
201
+ function bullet(el, on, na) {
202
+ el.className = 'bullet ' + (na ? 'bullet-na' : on ? 'bullet-on' : 'bullet-off');
203
+ }
204
+
205
+ function renderSnapshot(d) {
206
+ if (!d || !d.updatedAt) { offlineBanner.style.display = 'block'; return; }
207
+ offlineBanner.style.display = 'none';
208
+
209
+ var pct = d.context.percent || 0;
210
+ document.getElementById('ctx-pct').textContent = pct + '%';
211
+ var bar = document.getElementById('ctx-bar');
212
+ bar.style.width = Math.max(pct, 1) + '%';
213
+ bar.className = 'meter-fill ' + (pct >= 90 ? 'meter-red' : pct >= 70 ? 'meter-yellow' : 'meter-green');
214
+ var tok = d.context.tokens != null ? d.context.tokens.toLocaleString() : '?';
215
+ var win = d.context.contextWindow ? d.context.contextWindow.toLocaleString() : '?';
216
+ document.getElementById('ctx-sub').textContent = tok + ' / ' + win + ' tokens';
217
+
218
+ bullet(document.getElementById('tr-armed'), d.trigger.armed, false);
219
+ bullet(document.getElementById('tr-ready'), d.trigger.ready, !d.trigger.armed);
220
+ var state = d.trigger.ready ? 'THRESHOLD EXCEEDED — compacting next event' :
221
+ d.trigger.armed ? 'past fast gate — monitoring token count' : 'idle — below fast gate';
222
+ document.getElementById('tr-state').textContent = state;
223
+
224
+ document.getElementById('st-count').textContent = d.store.checkpointCount;
225
+ document.getElementById('st-tokens').textContent = d.store.totalTokenEstimate.toLocaleString();
226
+ document.getElementById('st-injected').textContent = d.store.injectedCount;
227
+ document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
228
+ document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
229
+
230
+ document.getElementById('cf-tier').textContent = d.tier;
231
+ document.getElementById('cf-threshold').textContent = d.config.thresholdTokens.toLocaleString();
232
+ document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
233
+ document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
234
+ document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
235
+
236
+ document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
237
+ }
238
+
239
+ function sanitize(s) {
240
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
241
+ }
242
+
243
+ function renderEvent(ev) {
244
+ evBuffer.unshift(ev);
245
+ if (evBuffer.length > MAX_EV) evBuffer.length = MAX_EV;
246
+ evBox.innerHTML = evBuffer.map(function(e) {
247
+ var t = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
248
+ var detail = '';
249
+ if (e.data) {
250
+ if (e.data.checkpointId) detail = sanitize(e.data.checkpointId);
251
+ else if (e.data.query) detail = sanitize(e.data.query.slice(0, 80));
252
+ if (e.data.tokenEstimate != null) detail += ' ' + e.data.tokenEstimate + ' tok';
253
+ if (e.data.deduped) detail += ' (deduped)';
254
+ if (e.data.injected != null) detail = 'injected: ' + e.data.injected + (e.data.empty ? ' (empty)' : '');
255
+ }
256
+ return '<div class="ev">' +
257
+ '<span class="ev-time">' + t + '</span>' +
258
+ '<span class="ev-type ev-type-' + sanitize(e.type) + '">' + sanitize(e.type) + '</span>' +
259
+ '<span class="ev-detail">' + detail + '</span></div>';
260
+ }).join('');
261
+ }
262
+
263
+ // Poll snapshot every 2s
264
+ function pollSnapshot() {
265
+ fetch('/api/snapshot').then(function(r) { return r.json(); }).then(renderSnapshot).catch(function() {});
266
+ }
267
+ pollSnapshot();
268
+ setInterval(pollSnapshot, 2000);
269
+
270
+ // SSE for events
271
+ function connectSSE() {
272
+ var es = new EventSource('/api/events');
273
+ es.onmessage = function(msg) {
274
+ try { renderEvent(JSON.parse(msg.data)); } catch(e) {}
275
+ };
276
+ es.onerror = function() {
277
+ es.close();
278
+ setTimeout(connectSSE, 3000);
279
+ };
280
+ }
281
+ connectSSE();
282
+ })();
283
+ </script>
284
+ </body>
285
+ </html>`;
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Server
290
+ // ---------------------------------------------------------------------------
291
+
292
+ export function launchDashboardServer(stateDir: string): Promise<{ port: number; url: string }> {
293
+ const portFile = join(stateDir, "port.pid");
294
+ const snapshotPath = join(stateDir, "dashboard.json");
295
+ const eventsPath = join(stateDir, "events.log");
296
+
297
+ // ── Existing server? ──────────────────────────────────────────────────────
298
+ if (existsSync(portFile)) {
299
+ try {
300
+ const info = JSON.parse(readFileSync(portFile, "utf-8"));
301
+ if (info && info.port) {
302
+ return Promise.resolve({ port: info.port, url: `http://localhost:${info.port}` });
303
+ }
304
+ } catch {
305
+ // stale file, overwrite
306
+ }
307
+ }
308
+
309
+ // ── New server ────────────────────────────────────────────────────────────
310
+ mkdirSync(stateDir, { recursive: true });
311
+
312
+ let eventOffset = 0;
313
+
314
+ const server = createServer((req: IncomingMessage, res: ServerResponse) => {
315
+ // CORS for local access
316
+ res.setHeader("Access-Control-Allow-Origin", "*");
317
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
318
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
319
+
320
+ if (req.method === "OPTIONS") {
321
+ res.writeHead(204);
322
+ res.end();
323
+ return;
324
+ }
325
+
326
+ if (req.url === "/" || req.url === "/index.html") {
327
+ const tier = readSnapshot(snapshotPath).tier;
328
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
329
+ res.end(dashboardHtml(tier));
330
+ return;
331
+ }
332
+
333
+ if (req.url === "/api/snapshot") {
334
+ const snap = readSnapshot(snapshotPath);
335
+ res.writeHead(200, { "Content-Type": "application/json" });
336
+ res.end(JSON.stringify(snap));
337
+ return;
338
+ }
339
+
340
+ if (req.url === "/api/events") {
341
+ res.writeHead(200, {
342
+ "Content-Type": "text/event-stream",
343
+ "Cache-Control": "no-cache",
344
+ "Connection": "keep-alive",
345
+ });
346
+
347
+ // Drain existing events so the client starts with history
348
+ const { data: existing, offset: initialOffset } = readFrom(eventsPath, 0);
349
+ eventOffset = initialOffset;
350
+ const lines = existing.split("\n").filter((l: string) => l.trim());
351
+ for (const line of lines) {
352
+ res.write(`data: ${line}\n\n`);
353
+ }
354
+
355
+ // Tail new events via fs.watch (coalesced with 100ms debounce)
356
+ let watchTimer: ReturnType<typeof setTimeout> | null = null;
357
+ const onWatch = () => {
358
+ if (watchTimer) return;
359
+ watchTimer = setTimeout(() => {
360
+ watchTimer = null;
361
+ const { data, offset } = readFrom(eventsPath, eventOffset);
362
+ eventOffset = offset;
363
+ const newLines = data.split("\n").filter((l: string) => l.trim());
364
+ for (const line of newLines) {
365
+ res.write(`data: ${line}\n\n`);
366
+ }
367
+ }, 100);
368
+ };
369
+
370
+ // Set up file watching: if file exists, watch it directly;
371
+ // otherwise poll for creation every 1s then switch to fs.watch.
372
+ let watcher: ReturnType<typeof watch> | null = null;
373
+ let pollInterval: ReturnType<typeof setInterval> | null = null;
374
+
375
+ function startFileWatch(): void {
376
+ try {
377
+ watcher = watch(eventsPath, onWatch);
378
+ } catch { /* give up */ }
379
+ }
380
+
381
+ if (existsSync(eventsPath)) {
382
+ startFileWatch();
383
+ } else {
384
+ pollInterval = setInterval(() => {
385
+ if (existsSync(eventsPath)) {
386
+ if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
387
+ startFileWatch();
388
+ }
389
+ }, 1000);
390
+ }
391
+
392
+ req.on("close", () => {
393
+ if (watchTimer) clearTimeout(watchTimer);
394
+ if (pollInterval) clearInterval(pollInterval);
395
+ watcher?.close();
396
+ });
397
+ return;
398
+ }
399
+
400
+ // Fallback — serve the dashboard
401
+ const tier = readSnapshot(snapshotPath).tier;
402
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
403
+ res.end(dashboardHtml(tier));
404
+ });
405
+
406
+ const TARGET_PORT = 9320;
407
+ const PORT_RANGE = 10; // 9320–9329
408
+
409
+ return new Promise((resolve, reject) => {
410
+ function tryPort(port: number) {
411
+ server.once("error", (err: NodeJS.ErrnoException) => {
412
+ if (err.code === "EADDRINUSE" && port < TARGET_PORT + PORT_RANGE - 1) {
413
+ tryPort(port + 1);
414
+ } else {
415
+ reject(err);
416
+ }
417
+ });
418
+
419
+ server.listen(port, "127.0.0.1", () => {
420
+ const url = `http://localhost:${port}`;
421
+ console.log(`[mega-compact] dashboard server running: ${url}`);
422
+
423
+ // Write port.pid
424
+ try {
425
+ writeFileSync(portFile, JSON.stringify({ port, pid: process.pid }));
426
+ } catch { /* non-fatal */ }
427
+
428
+ // Graceful cleanup
429
+ const cleanup = () => {
430
+ try { unlinkSync(portFile); } catch { /* already gone */ }
431
+ server.close();
432
+ process.exit(0);
433
+ };
434
+ process.on("SIGTERM", cleanup);
435
+ process.on("SIGINT", cleanup);
436
+
437
+ resolve({ port, url });
438
+ });
439
+ }
440
+
441
+ tryPort(TARGET_PORT);
442
+ });
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // CLI entry point — when run directly as `node dashboard-server.js <stateDir>`
447
+ // ---------------------------------------------------------------------------
448
+
449
+ if (process.argv[1] && process.argv[1].includes("dashboard-server")) {
450
+ const stateDir = process.argv[2];
451
+ if (!stateDir) {
452
+ console.error("Usage: node dashboard-server.js <stateDir>");
453
+ process.exit(1);
454
+ }
455
+ launchDashboardServer(stateDir).catch((err) => {
456
+ console.error("[mega-compact] dashboard server failed:", err);
457
+ process.exit(1);
458
+ });
459
+ }
@@ -0,0 +1,175 @@
1
+ // Error pattern detection for broken environment setups
2
+ // Detects various states of incomplete or broken configurations
3
+
4
+ interface FileCheckResult {
5
+ exists: boolean;
6
+ path: string;
7
+ }
8
+
9
+ interface PartialSetupState {
10
+ directoryExists: boolean;
11
+ requiredFiles: FileCheckResult[];
12
+ partialMarkerExists: boolean;
13
+ }
14
+
15
+ export function detectBrokenEnvironmentSetup(
16
+ configPath: string,
17
+ missingFileExists: boolean = false
18
+ ): boolean {
19
+ // Pattern 1: Checks if required config files are missing
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const configDir = path.dirname(configPath);
24
+ const requiredFiles = [
25
+ 'config.json',
26
+ 'settings.json',
27
+ '.env',
28
+ 'package.json',
29
+ ];
30
+
31
+ const directoryExists = fs.existsSync(configDir);
32
+ const allFilesExist = requiredFiles.every(file =>
33
+ fs.existsSync(path.join(configDir, file))
34
+ );
35
+
36
+ return directoryExists && !allFilesExist && !missingFileExists;
37
+ }
38
+
39
+ export function detectBrokenEnvironmentSetupWithLogging(
40
+ configPath: string,
41
+ missingFileExists: boolean = false
42
+ ): boolean {
43
+ // Pattern 2: Similar to pattern 1 but with verbose logging for debugging
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+
47
+ const configDir = path.dirname(configPath);
48
+ const requiredFiles = [
49
+ 'config.json',
50
+ 'settings.json',
51
+ '.env',
52
+ 'package.json',
53
+ ];
54
+
55
+ console.log(`[ERROR_PATTERN] Checking environment at: ${configDir}`);
56
+
57
+ const directoryExists = fs.existsSync(configDir);
58
+ console.log(`[ERROR_PATTERN] Directory exists: ${directoryExists}`);
59
+
60
+ const fileStatus = requiredFiles.map(file => ({
61
+ file,
62
+ exists: fs.existsSync(path.join(configDir, file)),
63
+ }));
64
+
65
+ console.log(`[ERROR_PATTERN] File status:`, fileStatus);
66
+
67
+ const allFilesExist = fileStatus.every(f => f.exists);
68
+ const result = directoryExists && !allFilesExist && !missingFileExists;
69
+
70
+ console.log(`[ERROR_PATTERN] Broken environment detected: ${result}`);
71
+ return result;
72
+ }
73
+
74
+ export function detectPartialEnvironmentSetup(
75
+ configPath: string,
76
+ incompleteFileExists: boolean = true
77
+ ): boolean {
78
+ // Pattern 3: Detects a variant where only some files exist
79
+ // and a partial setup marker indicates the setup was interrupted
80
+ const fs = require('fs');
81
+ const path = require('path');
82
+
83
+ const configDir = path.dirname(configPath);
84
+
85
+ // Files that indicate a complete setup was attempted
86
+ const setupPhaseFiles = [
87
+ 'package.json', // Phase 1: Project initialization
88
+ 'tsconfig.json', // Phase 2: TypeScript configuration
89
+ '.gitignore', // Phase 3: Git setup
90
+ 'README.md', // Phase 4: Documentation
91
+ ];
92
+
93
+ // Marker file that indicates setup started but wasn't completed
94
+ const partialMarker = '.setup-in-progress';
95
+ const partialMarkerPath = path.join(configDir, partialMarker);
96
+
97
+ console.log(`[PARTIAL_SETUP] Checking for incomplete setup at: ${configDir}`);
98
+
99
+ const directoryExists = fs.existsSync(configDir);
100
+ if (!directoryExists) {
101
+ console.log(`[PARTIAL_SETUP] Directory does not exist`);
102
+ return false;
103
+ }
104
+
105
+ const partialMarkerExists = fs.existsSync(partialMarkerPath);
106
+ console.log(`[PARTIAL_SETUP] Partial marker exists: ${partialMarkerExists}`);
107
+
108
+ const fileCheckResults: FileCheckResult[] = setupPhaseFiles.map(file => ({
109
+ exists: fs.existsSync(path.join(configDir, file)),
110
+ path: file,
111
+ }));
112
+
113
+ const existingFiles = fileCheckResults.filter(f => f.exists);
114
+ const missingFiles = fileCheckResults.filter(f => !f.exists);
115
+
116
+ console.log(`[PARTIAL_SETUP] Existing files: ${existingFiles.map(f => f.path).join(', ')}`);
117
+ console.log(`[PARTIAL_SETUP] Missing files: ${missingFiles.map(f => f.path).join(', ')}`);
118
+
119
+ // Detect partial setup: some files exist but not all,
120
+ // and either the partial marker exists or incomplete file flag is set
121
+ const hasPartialState = existingFiles.length > 0 && existingFiles.length < setupPhaseFiles.length;
122
+ const isPartialSetup = hasPartialState && (partialMarkerExists || incompleteFileExists);
123
+
124
+ console.log(`[PARTIAL_SETUP] Partial state detected: ${hasPartialState}`);
125
+ console.log(`[PARTIAL_SETUP] Is partial setup: ${isPartialSetup}`);
126
+
127
+ return isPartialSetup;
128
+ }
129
+
130
+ // Helper function to get detailed partial setup information
131
+ export function getPartialSetupDetails(
132
+ configPath: string
133
+ ): PartialSetupState | null {
134
+ const fs = require('fs');
135
+ const path = require('path');
136
+
137
+ const configDir = path.dirname(configPath);
138
+ const setupPhaseFiles = ['package.json', 'tsconfig.json', '.gitignore', 'README.md'];
139
+ const partialMarker = '.setup-in-progress';
140
+
141
+ if (!fs.existsSync(configDir)) {
142
+ return null;
143
+ }
144
+
145
+ return {
146
+ directoryExists: true,
147
+ requiredFiles: setupPhaseFiles.map(file => ({
148
+ exists: fs.existsSync(path.join(configDir, file)),
149
+ path: path.join(configDir, file),
150
+ })),
151
+ partialMarkerExists: fs.existsSync(path.join(configDir, partialMarker)),
152
+ };
153
+ }
154
+
155
+ // Function to create a partial setup state for testing
156
+ export function createPartialSetupForTesting(
157
+ testDir: string,
158
+ filesToCreate: string[]
159
+ ): void {
160
+ const fs = require('fs');
161
+ const path = require('path');
162
+
163
+ if (!fs.existsSync(testDir)) {
164
+ fs.mkdirSync(testDir, { recursive: true });
165
+ }
166
+
167
+ // Create the partial marker file
168
+ fs.writeFileSync(path.join(testDir, '.setup-in-progress'), 'Setup in progress...');
169
+
170
+ // Create only the specified files
171
+ filesToCreate.forEach(file => {
172
+ const filePath = path.join(testDir, file);
173
+ fs.writeFileSync(filePath, `// ${file} content`);
174
+ });
175
+ }