pi-mega-compact 0.8.21 → 0.8.23
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 +6 -2
- package/README.md +1 -1
- package/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
- package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
- package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
- package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
- package/dist/extensions/dashboard-server/html.js +2 -621
- package/dist/extensions/dashboard-server/routes-core.js +62 -0
- package/dist/extensions/dashboard-server/routes-game.js +323 -0
- package/dist/extensions/dashboard-server/routes-repo.js +170 -0
- package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
- package/dist/extensions/dashboard-server/routes.js +10 -0
- package/dist/extensions/dashboard-server/server.js +26 -623
- package/dist/extensions/mega-commands.js +4 -3
- package/dist/extensions/mega-events/agent-handlers.js +2 -1
- package/dist/extensions/mega-events/compact-handlers.js +26 -0
- package/dist/extensions/mega-events/session-handlers.js +2 -1
- package/dist/extensions/mega-pipeline/compact.js +3 -2
- package/dist/extensions/mega-runtime/state.js +7 -7
- package/dist/src/dedup/raptor/multilevel.js +172 -0
- package/dist/src/dedup/raptor/multilevel.test.js +203 -0
- package/dist/src/dedup/raptor/promote.test.js +5 -5
- package/dist/src/dedup/raptor/retrieval.js +1 -1
- package/dist/src/dedup/sprint12.test.js +7 -7
- package/dist/src/dedup-engine.test.js +29 -29
- package/dist/src/e2e.test.js +38 -38
- package/dist/src/engine.js +3 -3
- package/dist/src/engine.test.js +6 -6
- package/dist/src/importance.js +197 -0
- package/dist/src/importance.test.js +372 -0
- package/dist/src/ratio.bench.test.js +18 -18
- package/dist/src/recall.js +6 -5
- package/dist/src/recall.test.js +85 -27
- package/dist/src/sprint14.test.js +2 -2
- package/dist/src/store/migrate.test.js +5 -5
- package/dist/src/store/sprint10.test.js +5 -5
- package/dist/src/store/sqlite/global-index.js +5 -174
- package/dist/src/store/sqlite/global-sessions.js +190 -0
- package/dist/src/vector-read.js +168 -0
- package/dist/src/vector-search.js +191 -0
- package/dist/src/vectorStore.js +10 -297
- package/dist/src/vectorStore.test.js +32 -32
- package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
- package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
- package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
- package/extensions/dashboard-server/dashboard-client.ts +21 -0
- package/extensions/dashboard-server/html.ts +2 -621
- package/extensions/dashboard-server/routes-core.ts +113 -0
- package/extensions/dashboard-server/routes-game.ts +386 -0
- package/extensions/dashboard-server/routes-repo.ts +212 -0
- package/extensions/dashboard-server/routes-sessions.ts +195 -0
- package/extensions/dashboard-server/routes.ts +13 -0
- package/extensions/dashboard-server/server.ts +37 -700
- package/extensions/mega-commands.ts +4 -3
- package/extensions/mega-events/agent-handlers.ts +2 -1
- package/extensions/mega-events/compact-handlers.ts +28 -0
- package/extensions/mega-events/session-handlers.ts +2 -1
- package/extensions/mega-pipeline/compact.ts +3 -2
- package/extensions/mega-runtime/state.ts +7 -7
- package/extensions/openclaw-mega-compact.ts +2 -2
- package/package.json +2 -2
- package/src/dedup/raptor/multilevel.test.ts +278 -0
- package/src/dedup/raptor/multilevel.ts +246 -0
- package/src/dedup/raptor/promote.test.ts +5 -5
- package/src/dedup/raptor/retrieval.ts +1 -1
- package/src/dedup/sprint12.test.ts +7 -7
- package/src/dedup-engine.test.ts +30 -30
- package/src/e2e.test.ts +38 -38
- package/src/engine.test.ts +6 -6
- package/src/engine.ts +3 -3
- package/src/importance.test.ts +538 -0
- package/src/importance.ts +312 -0
- package/src/ratio.bench.test.ts +18 -18
- package/src/recall.test.ts +101 -29
- package/src/recall.ts +9 -9
- package/src/sprint14.test.ts +2 -2
- package/src/store/migrate.test.ts +5 -5
- package/src/store/sprint10.test.ts +5 -5
- package/src/store/sqlite/global-index.ts +18 -290
- package/src/store/sqlite/global-sessions.ts +291 -0
- package/src/vector-read.ts +237 -0
- package/src/vector-search.ts +231 -0
- package/src/vectorStore.test.ts +32 -32
- package/src/vectorStore.ts +29 -356
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-repo.ts — Repo-index and static route handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
+
|
|
9
|
+
import { readSnapshot } from "./snapshot.js";
|
|
10
|
+
import { dashboardHtml } from "./html.js";
|
|
11
|
+
import { readIndex, getIndexDir } from "./index-reader.js";
|
|
12
|
+
import { ACTIVE_WINDOW_SEC } from "./types.js";
|
|
13
|
+
import type { RouteContext } from "./routes-core.js";
|
|
14
|
+
import type { LiveSnapshot } from "./types.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// handleIndex — "/" | "/index.html" | "/api/snapshot" | "/api/version"
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export function handleIndex(
|
|
21
|
+
req: IncomingMessage,
|
|
22
|
+
res: ServerResponse,
|
|
23
|
+
ctx: RouteContext,
|
|
24
|
+
): boolean {
|
|
25
|
+
const { snapshotPath, SERVER_VERSION, serveClientAsset } = ctx;
|
|
26
|
+
|
|
27
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
28
|
+
// Sprint B1: prefer the React client build when present; fall back to the
|
|
29
|
+
// legacy inline html.ts template when the client dist is absent.
|
|
30
|
+
if (serveClientAsset("/", res)) return true;
|
|
31
|
+
const tier = readSnapshot(snapshotPath).tier;
|
|
32
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
33
|
+
res.end(dashboardHtml(tier));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (req.url === "/api/snapshot") {
|
|
38
|
+
const snap = readSnapshot(snapshotPath);
|
|
39
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify(snap));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Server version — lets the /dashboard launcher detect a stale server from
|
|
45
|
+
// an older build and replace it on upgrade rather than reuse it.
|
|
46
|
+
if (req.url === "/api/version") {
|
|
47
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
48
|
+
res.end(JSON.stringify({ version: SERVER_VERSION }));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// handleRepoIndex — "/api/index" | "/api/repos" | "/api/summary" | "/api/drift" | "/api/servers"
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export function handleRepoIndex(
|
|
60
|
+
req: IncomingMessage,
|
|
61
|
+
res: ServerResponse,
|
|
62
|
+
ctx: RouteContext,
|
|
63
|
+
): boolean {
|
|
64
|
+
const { overlayCurrentRepo, detectCrossRepoDrift } = ctx;
|
|
65
|
+
|
|
66
|
+
if (req.url === "/api/index") {
|
|
67
|
+
const idx = readIndex();
|
|
68
|
+
if (idx) overlayCurrentRepo(idx);
|
|
69
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
70
|
+
res.end(
|
|
71
|
+
JSON.stringify(idx ?? { updatedAt: null, summary: null, repos: [] }),
|
|
72
|
+
);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// /api/repos — registry list. Optional `?active=24h` filters to repos
|
|
77
|
+
// seen within the last N hours (default: all). The dashboard uses this to
|
|
78
|
+
// drive its "active vs archived" badge without refetching /api/index.
|
|
79
|
+
if (req.url?.startsWith("/api/repos")) {
|
|
80
|
+
const url = new URL(req.url, "http://x"); // guardrails-allow PREVENT-PI-004: localhost dashboard URL base (loopback-only)
|
|
81
|
+
const activeParam = url.searchParams.get("active");
|
|
82
|
+
const idx = readIndex();
|
|
83
|
+
if (idx) overlayCurrentRepo(idx);
|
|
84
|
+
let repos = idx?.repos ?? [];
|
|
85
|
+
if (activeParam) {
|
|
86
|
+
const m = /^(\d+)h$/.exec(activeParam);
|
|
87
|
+
if (m) {
|
|
88
|
+
const cutoffSec = Math.floor(Date.now() / 1000) - Number(m[1]) * 3600;
|
|
89
|
+
repos = repos.filter((r) => (r.lastSeen ?? 0) >= cutoffSec);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
93
|
+
res.end(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
updatedAt: idx?.updatedAt ?? null,
|
|
96
|
+
repos,
|
|
97
|
+
count: repos.length,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// /api/summary — header tiles without the full repo list (keeps payload
|
|
104
|
+
// small for embed scenarios). activeRepos mirrors the /api/repos?active=24h
|
|
105
|
+
// count so the dashboard can render the active badge alongside totals.
|
|
106
|
+
if (req.url?.startsWith("/api/summary")) {
|
|
107
|
+
const idx = readIndex();
|
|
108
|
+
if (idx) overlayCurrentRepo(idx);
|
|
109
|
+
const repos = idx?.repos ?? [];
|
|
110
|
+
const cutoffSec = Math.floor(Date.now() / 1000) - 24 * 3600;
|
|
111
|
+
const activeRepos = repos.filter(
|
|
112
|
+
(r) => (r.lastSeen ?? 0) >= cutoffSec,
|
|
113
|
+
).length;
|
|
114
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
115
|
+
res.end(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
updatedAt: idx?.updatedAt ?? null,
|
|
118
|
+
summary: idx?.summary ?? null,
|
|
119
|
+
activeRepos,
|
|
120
|
+
totalRepos: repos.length,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// /api/drift — R4: cross-repo drift report over repo_registry. Flags stale
|
|
127
|
+
// repos (>30d idle), compaction lag (active but >24h since last
|
|
128
|
+
// compaction), and recent model churn. Read-only.
|
|
129
|
+
if (req.url?.startsWith("/api/drift")) {
|
|
130
|
+
const report = detectCrossRepoDrift(getIndexDir());
|
|
131
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify(report));
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (req.url === "/api/servers") {
|
|
137
|
+
try {
|
|
138
|
+
const idx = readIndex();
|
|
139
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
140
|
+
const servers = (idx?.repos ?? [])
|
|
141
|
+
.filter((r) => (r.lastSeen ?? 0) >= nowSec - ACTIVE_WINDOW_SEC)
|
|
142
|
+
.map((r) => {
|
|
143
|
+
const out: Record<string, unknown> = {
|
|
144
|
+
repoRoot: r.repoRoot,
|
|
145
|
+
displayName: r.displayName,
|
|
146
|
+
model: r.modelName,
|
|
147
|
+
provider: r.providerName,
|
|
148
|
+
lastSeen: r.lastSeen,
|
|
149
|
+
lastCompactedAt: r.lastCompactedAt,
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
const p = join(r.stateDir, "dashboard.json");
|
|
153
|
+
if (existsSync(p)) {
|
|
154
|
+
const snap = JSON.parse(
|
|
155
|
+
readFileSync(p, "utf-8"),
|
|
156
|
+
) as LiveSnapshot;
|
|
157
|
+
out.tier = snap.tier ?? null;
|
|
158
|
+
out.contextPct =
|
|
159
|
+
snap.context && snap.context.percent != null
|
|
160
|
+
? snap.context.percent
|
|
161
|
+
: null;
|
|
162
|
+
out.state = (snap.session && snap.session.state) || null;
|
|
163
|
+
out.cacheHits = snap.cacheHits ?? null;
|
|
164
|
+
out.compacts = snap.compacts ?? null;
|
|
165
|
+
out.timeSaved = snap.timeSaved ?? null;
|
|
166
|
+
out.updatedAt = snap.updatedAt ?? null;
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
/* best-effort */
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
})
|
|
173
|
+
.sort((a, b) => (b.lastSeen as number) - (a.lastSeen as number));
|
|
174
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
175
|
+
res.end(
|
|
176
|
+
JSON.stringify({ updatedAt: new Date().toISOString(), servers }),
|
|
177
|
+
);
|
|
178
|
+
} catch {
|
|
179
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
180
|
+
res.end(JSON.stringify({ error: "servers_unavailable" }));
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// handleStatic — non-/api/* GET fallback (SPA client build or legacy HTML).
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
export function handleStatic(
|
|
193
|
+
req: IncomingMessage,
|
|
194
|
+
res: ServerResponse,
|
|
195
|
+
_ctx: RouteContext,
|
|
196
|
+
): boolean {
|
|
197
|
+
const { snapshotPath, serveClientAsset } = _ctx;
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
req.method === "GET" &&
|
|
201
|
+
req.url &&
|
|
202
|
+
!req.url.startsWith("/api/") &&
|
|
203
|
+
serveClientAsset(req.url, res)
|
|
204
|
+
) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
// Legacy SPA fallback — serve inline HTML with current tier.
|
|
208
|
+
const tier = readSnapshot(snapshotPath).tier;
|
|
209
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
210
|
+
res.end(dashboardHtml(tier));
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-sessions.ts — Session and SSE event route handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { existsSync, watch } from "node:fs";
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
+
|
|
9
|
+
import { readFrom } from "./snapshot.js";
|
|
10
|
+
import type { RouteContext } from "./routes-core.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// handleEvents — "/api/events" SSE
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export function handleEvents(
|
|
17
|
+
req: IncomingMessage,
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
ctx: RouteContext,
|
|
20
|
+
): boolean {
|
|
21
|
+
if (req.url !== "/api/events") return false;
|
|
22
|
+
|
|
23
|
+
const { eventsPath, eventOffsetRef } = ctx;
|
|
24
|
+
|
|
25
|
+
res.writeHead(200, {
|
|
26
|
+
"Content-Type": "text/event-stream",
|
|
27
|
+
"Cache-Control": "no-cache",
|
|
28
|
+
Connection: "keep-alive",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Drain existing events so the client starts with history
|
|
32
|
+
const { data: existing, offset: initialOffset } = readFrom(eventsPath, 0);
|
|
33
|
+
eventOffsetRef.value = initialOffset;
|
|
34
|
+
const lines = existing.split("\n").filter((l: string) => l.trim());
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
res.write(`data: ${line}\n\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Tail new events via fs.watch (coalesced with 100ms debounce)
|
|
40
|
+
let watchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
const onWatch = () => {
|
|
42
|
+
if (watchTimer) return;
|
|
43
|
+
watchTimer = setTimeout(() => {
|
|
44
|
+
watchTimer = null;
|
|
45
|
+
const { data, offset } = readFrom(eventsPath, eventOffsetRef.value);
|
|
46
|
+
eventOffsetRef.value = offset;
|
|
47
|
+
const newLines = data.split("\n").filter((l: string) => l.trim());
|
|
48
|
+
for (const line of newLines) {
|
|
49
|
+
res.write(`data: ${line}\n\n`);
|
|
50
|
+
}
|
|
51
|
+
}, 100);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Set up file watching: if file exists, watch it directly;
|
|
55
|
+
// otherwise poll for creation every 1s then switch to fs.watch.
|
|
56
|
+
let watcher: ReturnType<typeof watch> | null = null;
|
|
57
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
58
|
+
|
|
59
|
+
function startFileWatch(): void {
|
|
60
|
+
try {
|
|
61
|
+
watcher = watch(eventsPath, onWatch);
|
|
62
|
+
} catch {
|
|
63
|
+
/* give up */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (existsSync(eventsPath)) {
|
|
68
|
+
startFileWatch();
|
|
69
|
+
} else {
|
|
70
|
+
pollInterval = setInterval(() => {
|
|
71
|
+
if (existsSync(eventsPath)) {
|
|
72
|
+
if (pollInterval) {
|
|
73
|
+
clearInterval(pollInterval);
|
|
74
|
+
pollInterval = null;
|
|
75
|
+
}
|
|
76
|
+
startFileWatch();
|
|
77
|
+
}
|
|
78
|
+
}, 1000);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
req.on("close", () => {
|
|
82
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
83
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
84
|
+
watcher?.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// handleSessions — "/api/sessions" (GET sessions list) and
|
|
92
|
+
// "/api/sessions/timeseries" (GET timeseries).
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export function handleSessions(
|
|
96
|
+
req: IncomingMessage,
|
|
97
|
+
res: ServerResponse,
|
|
98
|
+
_ctx: RouteContext,
|
|
99
|
+
): boolean {
|
|
100
|
+
if (!req.url?.startsWith("/api/sessions")) return false;
|
|
101
|
+
|
|
102
|
+
// /api/sessions/timeseries handled inline below.
|
|
103
|
+
if (req.url.startsWith("/api/sessions/timeseries")) {
|
|
104
|
+
// /api/sessions/timeseries — S39: stacked per-session token timeseries for
|
|
105
|
+
// the recharts memory graph. GET ?minutes=N (clamped [1,1440]) returns
|
|
106
|
+
// {updatedAt, windowMinutes, series[], totals[]} in recharts-ready shape.
|
|
107
|
+
// Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
108
|
+
const tsReq = createRequire(import.meta.url);
|
|
109
|
+
const {
|
|
110
|
+
readSessionTimeseries,
|
|
111
|
+
pruneTokenSamples,
|
|
112
|
+
} = tsReq(
|
|
113
|
+
"../../src/store/sqlite.js",
|
|
114
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
115
|
+
if (req.method !== "GET") {
|
|
116
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
117
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const url = new URL(req.url, "http://x"); // guardrails-allow PREVENT-PI-004: localhost dashboard URL base (loopback-only)
|
|
122
|
+
let minutes = Number(url.searchParams.get("minutes") ?? "30");
|
|
123
|
+
if (!Number.isFinite(minutes) || minutes <= 0) minutes = 30;
|
|
124
|
+
minutes = Math.min(Math.max(minutes, 1), 1440);
|
|
125
|
+
const sinceTs = Date.now() - minutes * 60_000;
|
|
126
|
+
const pruneMs = Math.max(minutes * 60_000, 1_800_000);
|
|
127
|
+
pruneTokenSamples(pruneMs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
128
|
+
const result = readSessionTimeseries(sinceTs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
129
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
130
|
+
res.end(
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
updatedAt: new Date().toISOString(),
|
|
133
|
+
windowMinutes: minutes,
|
|
134
|
+
series: result.series,
|
|
135
|
+
totals: result.totals,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
140
|
+
res.end(
|
|
141
|
+
JSON.stringify({ error: "timeseries_unavailable", detail: String(e) }),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// /api/sessions — S39: active pi sessions with latest token usage + heartbeat.
|
|
148
|
+
// GET returns {updatedAt, pruned, sessions[]} after pruning stale heartbeats.
|
|
149
|
+
// The dashboard server is a detached child with no MegaRuntime ref, so it
|
|
150
|
+
// reads session_heartbeats via a require()'d sqlite helper (same pattern as
|
|
151
|
+
// /api/achievements, /api/perf). Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
152
|
+
const sReq = createRequire(import.meta.url);
|
|
153
|
+
const {
|
|
154
|
+
readActiveSessions,
|
|
155
|
+
pruneStaleSessions,
|
|
156
|
+
listRepoRegistry,
|
|
157
|
+
} = sReq(
|
|
158
|
+
"../../src/store/sqlite.js",
|
|
159
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
160
|
+
if (req.method !== "GET") {
|
|
161
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
162
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const pruned = pruneStaleSessions(); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
167
|
+
const active = readActiveSessions();
|
|
168
|
+
const repos = listRepoRegistry();
|
|
169
|
+
const repoMap = new Map(repos.map((r) => [r.repoRoot, r]));
|
|
170
|
+
const sessions = active.map((s) => ({
|
|
171
|
+
pid: s.pid,
|
|
172
|
+
sessionId: s.sessionId,
|
|
173
|
+
repoRoot: s.repoRoot,
|
|
174
|
+
displayName: s.repoRoot
|
|
175
|
+
? (s.repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? s.repoRoot)
|
|
176
|
+
: (s.stateDir?.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"),
|
|
177
|
+
model: s.repoRoot ? (repoMap.get(s.repoRoot)?.modelName ?? null) : null,
|
|
178
|
+
tokens: s.tokens,
|
|
179
|
+
percent: s.percent,
|
|
180
|
+
ctxWindow: s.ctxWindow,
|
|
181
|
+
lastSeen: s.lastSeen,
|
|
182
|
+
stateDir: s.stateDir,
|
|
183
|
+
}));
|
|
184
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
185
|
+
res.end(
|
|
186
|
+
JSON.stringify({ updatedAt: new Date().toISOString(), pruned, sessions }),
|
|
187
|
+
);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
190
|
+
res.end(
|
|
191
|
+
JSON.stringify({ error: "sessions_unavailable", detail: String(e) }),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes.ts — Barrel: re-exports all route handlers + types.
|
|
3
|
+
*
|
|
4
|
+
* server.ts imports only from this barrel; the actual handler implementations
|
|
5
|
+
* live in the split files below.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { buildRouteContext } from "./routes-core.js";
|
|
9
|
+
export type { RouteContext } from "./routes-core.js";
|
|
10
|
+
|
|
11
|
+
export { handleIndex, handleRepoIndex, handleStatic } from "./routes-repo.js";
|
|
12
|
+
export { handleGameState, handleGameScores, handlePerf, handleAchievements } from "./routes-game.js";
|
|
13
|
+
export { handleEvents, handleSessions } from "./routes-sessions.js";
|