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.
Files changed (84) hide show
  1. package/LICENSE +6 -2
  2. package/README.md +1 -1
  3. package/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
  4. package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
  5. package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
  6. package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
  7. package/dist/extensions/dashboard-server/html.js +2 -621
  8. package/dist/extensions/dashboard-server/routes-core.js +62 -0
  9. package/dist/extensions/dashboard-server/routes-game.js +323 -0
  10. package/dist/extensions/dashboard-server/routes-repo.js +170 -0
  11. package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
  12. package/dist/extensions/dashboard-server/routes.js +10 -0
  13. package/dist/extensions/dashboard-server/server.js +26 -623
  14. package/dist/extensions/mega-commands.js +4 -3
  15. package/dist/extensions/mega-events/agent-handlers.js +2 -1
  16. package/dist/extensions/mega-events/compact-handlers.js +26 -0
  17. package/dist/extensions/mega-events/session-handlers.js +2 -1
  18. package/dist/extensions/mega-pipeline/compact.js +3 -2
  19. package/dist/extensions/mega-runtime/state.js +7 -7
  20. package/dist/src/dedup/raptor/multilevel.js +172 -0
  21. package/dist/src/dedup/raptor/multilevel.test.js +203 -0
  22. package/dist/src/dedup/raptor/promote.test.js +5 -5
  23. package/dist/src/dedup/raptor/retrieval.js +1 -1
  24. package/dist/src/dedup/sprint12.test.js +7 -7
  25. package/dist/src/dedup-engine.test.js +29 -29
  26. package/dist/src/e2e.test.js +38 -38
  27. package/dist/src/engine.js +3 -3
  28. package/dist/src/engine.test.js +6 -6
  29. package/dist/src/importance.js +197 -0
  30. package/dist/src/importance.test.js +372 -0
  31. package/dist/src/ratio.bench.test.js +18 -18
  32. package/dist/src/recall.js +6 -5
  33. package/dist/src/recall.test.js +85 -27
  34. package/dist/src/sprint14.test.js +2 -2
  35. package/dist/src/store/migrate.test.js +5 -5
  36. package/dist/src/store/sprint10.test.js +5 -5
  37. package/dist/src/store/sqlite/global-index.js +5 -174
  38. package/dist/src/store/sqlite/global-sessions.js +190 -0
  39. package/dist/src/vector-read.js +168 -0
  40. package/dist/src/vector-search.js +191 -0
  41. package/dist/src/vectorStore.js +10 -297
  42. package/dist/src/vectorStore.test.js +32 -32
  43. package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
  44. package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
  45. package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
  46. package/extensions/dashboard-server/dashboard-client.ts +21 -0
  47. package/extensions/dashboard-server/html.ts +2 -621
  48. package/extensions/dashboard-server/routes-core.ts +113 -0
  49. package/extensions/dashboard-server/routes-game.ts +386 -0
  50. package/extensions/dashboard-server/routes-repo.ts +212 -0
  51. package/extensions/dashboard-server/routes-sessions.ts +195 -0
  52. package/extensions/dashboard-server/routes.ts +13 -0
  53. package/extensions/dashboard-server/server.ts +37 -700
  54. package/extensions/mega-commands.ts +4 -3
  55. package/extensions/mega-events/agent-handlers.ts +2 -1
  56. package/extensions/mega-events/compact-handlers.ts +28 -0
  57. package/extensions/mega-events/session-handlers.ts +2 -1
  58. package/extensions/mega-pipeline/compact.ts +3 -2
  59. package/extensions/mega-runtime/state.ts +7 -7
  60. package/extensions/openclaw-mega-compact.ts +2 -2
  61. package/package.json +2 -2
  62. package/src/dedup/raptor/multilevel.test.ts +278 -0
  63. package/src/dedup/raptor/multilevel.ts +246 -0
  64. package/src/dedup/raptor/promote.test.ts +5 -5
  65. package/src/dedup/raptor/retrieval.ts +1 -1
  66. package/src/dedup/sprint12.test.ts +7 -7
  67. package/src/dedup-engine.test.ts +30 -30
  68. package/src/e2e.test.ts +38 -38
  69. package/src/engine.test.ts +6 -6
  70. package/src/engine.ts +3 -3
  71. package/src/importance.test.ts +538 -0
  72. package/src/importance.ts +312 -0
  73. package/src/ratio.bench.test.ts +18 -18
  74. package/src/recall.test.ts +101 -29
  75. package/src/recall.ts +9 -9
  76. package/src/sprint14.test.ts +2 -2
  77. package/src/store/migrate.test.ts +5 -5
  78. package/src/store/sprint10.test.ts +5 -5
  79. package/src/store/sqlite/global-index.ts +18 -290
  80. package/src/store/sqlite/global-sessions.ts +291 -0
  81. package/src/vector-read.ts +237 -0
  82. package/src/vector-search.ts +231 -0
  83. package/src/vectorStore.test.ts +32 -32
  84. 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";