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.
- package/LICENSE +24 -0
- package/README.md +375 -0
- package/extensions/DASHBOARD.md +160 -0
- package/extensions/dashboard-server.test.ts +124 -0
- package/extensions/dashboard-server.ts +459 -0
- package/extensions/error-patterns.ts +175 -0
- package/extensions/mega-compact.test.ts +351 -0
- package/extensions/mega-compact.ts +846 -0
- package/extensions/openclaw-mega-compact.ts +370 -0
- package/package.json +61 -0
- package/src/adapt.ts +120 -0
- package/src/boundary.test.ts +61 -0
- package/src/boundary.ts +94 -0
- package/src/canary.ts +126 -0
- package/src/compact.test.ts +99 -0
- package/src/compact.ts +262 -0
- package/src/config/dedup.ts +120 -0
- package/src/config.ts +15 -0
- package/src/dedup/dedup.test.ts +46 -0
- package/src/dedup/digest.ts +40 -0
- package/src/dedup/l1-lsh.ts +67 -0
- package/src/dedup/l1-minhash.ts +90 -0
- package/src/dedup/l1-verify.ts +55 -0
- package/src/dedup/l1.test.ts +57 -0
- package/src/dedup/mmr.ts +54 -0
- package/src/dedup/normalize.ts +41 -0
- package/src/dedup/raptor/guardrails.ts +112 -0
- package/src/dedup/raptor/index.ts +118 -0
- package/src/dedup/raptor/kmeans.ts +156 -0
- package/src/dedup/raptor/raptor.test.ts +238 -0
- package/src/dedup/raptor/retrieval.ts +102 -0
- package/src/dedup/raptor/summarizer.ts +91 -0
- package/src/dedup/raptor/tree.ts +254 -0
- package/src/dedup/sprint12.test.ts +242 -0
- package/src/dedup/topk.ts +61 -0
- package/src/dedup-engine.test.ts +609 -0
- package/src/e2e.test.ts +843 -0
- package/src/embedder.ts +111 -0
- package/src/engine.test.ts +123 -0
- package/src/engine.ts +192 -0
- package/src/extractive.test.ts +156 -0
- package/src/extractive.ts +265 -0
- package/src/httpEmbedder.ts +154 -0
- package/src/log.test.ts +47 -0
- package/src/log.ts +60 -0
- package/src/monitoring.ts +171 -0
- package/src/ratio.bench.test.ts +1316 -0
- package/src/recall.integration.test.ts +96 -0
- package/src/recall.test.ts +59 -0
- package/src/recall.ts +100 -0
- package/src/sprint14.test.ts +245 -0
- package/src/store/backfill.ts +263 -0
- package/src/store/bloom.ts +122 -0
- package/src/store/compression.test.ts +83 -0
- package/src/store/compression.ts +203 -0
- package/src/store/integrity.ts +65 -0
- package/src/store/migrate.test.ts +158 -0
- package/src/store/migrate.ts +108 -0
- package/src/store/sprint10.test.ts +182 -0
- package/src/store/sqlite.ts +519 -0
- package/src/store.test.ts +169 -0
- package/src/store.ts +192 -0
- package/src/supersede.test.ts +42 -0
- package/src/supersede.ts +67 -0
- package/src/tokens.ts +35 -0
- package/src/types.test.ts +10 -0
- package/src/types.ts +49 -0
- package/src/vectorStore.test.ts +480 -0
- 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
+
}
|