pi-mega-compact 0.8.17 → 0.8.19
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/dist/extensions/dashboard-server/api-contracts/endpoints.js +12 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +19 -6
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/server.js +83 -0
- package/dist/extensions/mega-events/context-handler.js +22 -8
- package/dist/extensions/mega-runtime/state.js +17 -1
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/global-index.js +199 -1
- package/dist/src/store/sqlite/global-index.test.js +136 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js → AchievementTiles-DVgFowc-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js.map → AchievementTiles-DVgFowc-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-Bg86TWPU.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js.map → AchievementsTab-Bg86TWPU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js → CacheTab-DBzoH_q_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js.map → CacheTab-DBzoH_q_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DXD7zbqy.js → ConfigTab-Bj12LgTh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/ConfigTab-Bj12LgTh.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-EC0wMsp_.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-EC0wMsp_.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js → GameTab-CISQitbu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js.map → GameTab-CISQitbu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js → MetricsTab-us4vVG5D.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js.map → MetricsTab-us4vVG5D.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-eJ7vFgBE.js +2 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-eJ7vFgBE.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js → ReposTab-CiU9AJkb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js.map → ReposTab-CiU9AJkb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SessionsTab-DaCUvTC7.js +70 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-DaCUvTC7.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js → TimeSavedCard-BAUSCzq5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js.map → TimeSavedCard-BAUSCzq5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js → index-5MPZyayK.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js.map → index-5MPZyayK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Cfxniu2R.css → index-CDkmYP9l.css} +1 -1
- package/extensions/dashboard-client/dist/assets/useSSE-BSpmWZZi.js +2 -0
- package/extensions/dashboard-client/dist/assets/useSSE-BSpmWZZi.js.map +1 -0
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/package-lock.json +374 -2
- package/extensions/dashboard-client/package.json +2 -1
- package/extensions/dashboard-client/src/App.tsx +5 -1
- package/extensions/dashboard-client/src/api/client.ts +12 -0
- package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +185 -0
- package/extensions/dashboard-client/src/components/EventStream.tsx +3 -0
- package/extensions/dashboard-client/src/components/RepoContextStack.tsx +238 -0
- package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +199 -0
- package/extensions/dashboard-client/src/main.tsx +2 -0
- package/extensions/dashboard-client/src/styles/repostack.css +87 -0
- package/extensions/dashboard-client/src/styles/sessions.css +193 -0
- package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +6 -3
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +49 -1
- package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +193 -0
- package/extensions/dashboard-server/api-contracts/core.ts +25 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +102 -0
- package/extensions/dashboard-server/api-contracts/index.ts +10 -1
- package/extensions/dashboard-server/api-contracts.test.ts +20 -7
- package/extensions/dashboard-server/server.ts +100 -0
- package/extensions/mega-events/context-handler.ts +40 -18
- package/extensions/mega-runtime/state.ts +31 -0
- package/package.json +1 -1
- package/src/store/sqlite/global-index.test.ts +222 -0
- package/src/store/sqlite/global-index.ts +316 -1
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DKgqx1RQ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ConfigTab-DXD7zbqy.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js +0 -2
- package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-lVGR1Jl_.js +0 -2
- package/extensions/dashboard-client/dist/assets/OverviewTab-lVGR1Jl_.js.map +0 -1
|
@@ -336,6 +336,94 @@ export interface AchievementRow {
|
|
|
336
336
|
readonly unlocked_at: number | null;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
// ─── Sessions Memory Graph (S39) ─────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A single active session in the GET /api/sessions response. Represents a
|
|
343
|
+
* live pi process with its latest token usage and heartbeat.
|
|
344
|
+
*/
|
|
345
|
+
export interface ActiveSession {
|
|
346
|
+
/** OS process ID of the pi process. */
|
|
347
|
+
readonly pid: number;
|
|
348
|
+
/** Session identifier (normalized). */
|
|
349
|
+
readonly sessionId: string;
|
|
350
|
+
/** Absolute path to the repo root, or null if not in a git repo. */
|
|
351
|
+
readonly repoRoot: string | null;
|
|
352
|
+
/** Display name (repo basename or state dir basename). */
|
|
353
|
+
readonly displayName: string;
|
|
354
|
+
/** Model name, or null if not captured. Available from repo_registry join. */
|
|
355
|
+
readonly model: string | null;
|
|
356
|
+
/** Latest context token count, or null if no sample yet. */
|
|
357
|
+
readonly tokens: number | null;
|
|
358
|
+
/** Latest context pressure percentage (0–100), or null. */
|
|
359
|
+
readonly percent: number | null;
|
|
360
|
+
/** Context window size in tokens. */
|
|
361
|
+
readonly ctxWindow: number;
|
|
362
|
+
/** Unix timestamp (ms) of the last heartbeat. */
|
|
363
|
+
readonly lastSeen: number;
|
|
364
|
+
/** State directory of the session. */
|
|
365
|
+
readonly stateDir: string | null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Response for GET /api/sessions. Lists active sessions with their latest
|
|
370
|
+
* token usage after pruning stale entries.
|
|
371
|
+
*/
|
|
372
|
+
export interface SessionsResponse {
|
|
373
|
+
/** ISO timestamp when the response was generated. */
|
|
374
|
+
readonly updatedAt: string;
|
|
375
|
+
/** Number of stale sessions pruned during this request. */
|
|
376
|
+
readonly pruned: number;
|
|
377
|
+
/** Array of active sessions, sorted by lastSeen descending. */
|
|
378
|
+
readonly sessions: ActiveSession[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** A single time-series data point for a session. */
|
|
382
|
+
export interface SessionDataPoint {
|
|
383
|
+
/** Unix timestamp (ms). */
|
|
384
|
+
readonly ts: number;
|
|
385
|
+
/** Token count at this sample. */
|
|
386
|
+
readonly tokens: number;
|
|
387
|
+
/** Context pressure percentage (0–100). */
|
|
388
|
+
readonly percent: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A recharts-ready per-session series with a stable color. */
|
|
392
|
+
export interface SessionSeries {
|
|
393
|
+
/** Session identifier. */
|
|
394
|
+
readonly sessionId: string;
|
|
395
|
+
/** Short display label for the legend. */
|
|
396
|
+
readonly label: string;
|
|
397
|
+
/** Stable hex color string (e.g. "#60a5fa"). */
|
|
398
|
+
readonly color: string;
|
|
399
|
+
/** Data points ordered by timestamp ascending. */
|
|
400
|
+
readonly data: SessionDataPoint[];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Query parameters for GET /api/sessions/timeseries. The `minutes` parameter
|
|
405
|
+
* controls the rolling window size.
|
|
406
|
+
*/
|
|
407
|
+
export interface SessionTimeseriesQuery {
|
|
408
|
+
/** Rolling window size in minutes (default: 30, clamped to [1, 1440]). */
|
|
409
|
+
readonly minutes?: number;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Response for GET /api/sessions/timeseries. Returns recharts-ready stacked
|
|
414
|
+
* per-session series + a totals array.
|
|
415
|
+
*/
|
|
416
|
+
export interface SessionTimeseriesResponse {
|
|
417
|
+
/** ISO timestamp when the response was generated. */
|
|
418
|
+
readonly updatedAt: string;
|
|
419
|
+
/** Rolling window size in minutes. */
|
|
420
|
+
readonly windowMinutes: number;
|
|
421
|
+
/** Per-session series with stable colors. */
|
|
422
|
+
readonly series: SessionSeries[];
|
|
423
|
+
/** Totals (sum of all sessions) per timestamp. */
|
|
424
|
+
readonly totals: { readonly ts: number; readonly tokens: number }[];
|
|
425
|
+
}
|
|
426
|
+
|
|
339
427
|
// ─── Game State Patch (PUT request body) ────────────────────────────────────
|
|
340
428
|
|
|
341
429
|
/**
|
|
@@ -473,4 +561,18 @@ export const ENDPOINTS = {
|
|
|
473
561
|
path: '/api/achievements',
|
|
474
562
|
description: 'All achievement rows with unlock state.',
|
|
475
563
|
} as const satisfies EndpointDef<'GET', undefined, AchievementRow[]>,
|
|
564
|
+
|
|
565
|
+
/** GET /api/sessions — Active sessions with latest token usage (S39). */
|
|
566
|
+
sessions: {
|
|
567
|
+
method: 'GET',
|
|
568
|
+
path: '/api/sessions',
|
|
569
|
+
description: 'Active pi sessions with latest token usage + heartbeat.',
|
|
570
|
+
} as const satisfies EndpointDef<'GET', undefined, SessionsResponse>,
|
|
571
|
+
|
|
572
|
+
/** GET /api/sessions/timeseries — Stacked per-session token timeseries (S39). */
|
|
573
|
+
sessionTimeseries: {
|
|
574
|
+
method: 'GET',
|
|
575
|
+
path: '/api/sessions/timeseries',
|
|
576
|
+
description: 'Recharts-ready per-session token timeseries for the stacked memory graph.',
|
|
577
|
+
} as const satisfies EndpointDef<'GET', SessionTimeseriesQuery, SessionTimeseriesResponse>,
|
|
476
578
|
} as const;
|
|
@@ -26,6 +26,7 @@ export type {
|
|
|
26
26
|
SseCrewPresenceChanged,
|
|
27
27
|
SseCrewTurnChanged,
|
|
28
28
|
SseCrewBanditChosen,
|
|
29
|
+
SseSessionSample,
|
|
29
30
|
} from './core.js';
|
|
30
31
|
|
|
31
32
|
// Snapshot / store / compression / session
|
|
@@ -103,6 +104,7 @@ import type {
|
|
|
103
104
|
SseCrewPresenceChanged,
|
|
104
105
|
SseCrewTurnChanged,
|
|
105
106
|
SseCrewBanditChosen,
|
|
107
|
+
SseSessionSample,
|
|
106
108
|
} from './core.js';
|
|
107
109
|
|
|
108
110
|
// Endpoints registry (Sprint A1)
|
|
@@ -130,6 +132,12 @@ export type {
|
|
|
130
132
|
GameScoresQuery,
|
|
131
133
|
AchievementRow,
|
|
132
134
|
GameStatePatch,
|
|
135
|
+
ActiveSession,
|
|
136
|
+
SessionsResponse,
|
|
137
|
+
SessionDataPoint,
|
|
138
|
+
SessionSeries,
|
|
139
|
+
SessionTimeseriesQuery,
|
|
140
|
+
SessionTimeseriesResponse,
|
|
133
141
|
SseEndpointDef,
|
|
134
142
|
} from './endpoints.js';
|
|
135
143
|
|
|
@@ -156,4 +164,5 @@ export type SseEvent =
|
|
|
156
164
|
| SseGameRitualStage
|
|
157
165
|
| SseGameRitualEnd
|
|
158
166
|
| SseGameModeChanged
|
|
159
|
-
| SseGameRender
|
|
167
|
+
| SseGameRender
|
|
168
|
+
| SseSessionSample;
|
|
@@ -158,10 +158,11 @@ void _c_perf;
|
|
|
158
158
|
void _c_achievements;
|
|
159
159
|
|
|
160
160
|
// ─── Compile-Time: ENDPOINTS path/method consistency ─────────────────────────
|
|
161
|
-
// Verify the ENDPOINTS registry has exactly
|
|
161
|
+
// Verify the ENDPOINTS registry has exactly 15 entries with correct paths.
|
|
162
|
+
// (Sprint 39 added /api/sessions + /api/sessions/timeseries.)
|
|
162
163
|
|
|
163
164
|
const ENDPOINT_KEYS = Object.keys(ENDPOINTS) as (keyof typeof ENDPOINTS)[];
|
|
164
|
-
const EXPECTED_ENDPOINT_COUNT =
|
|
165
|
+
const EXPECTED_ENDPOINT_COUNT = 15;
|
|
165
166
|
|
|
166
167
|
/** All `/api/*` paths served by server.ts (extracted from the route handlers). */
|
|
167
168
|
const SERVER_TS_PATHS: string[] = [
|
|
@@ -177,12 +178,14 @@ const SERVER_TS_PATHS: string[] = [
|
|
|
177
178
|
"/api/game-scores",
|
|
178
179
|
"/api/perf",
|
|
179
180
|
"/api/achievements",
|
|
181
|
+
"/api/sessions",
|
|
182
|
+
"/api/sessions/timeseries",
|
|
180
183
|
];
|
|
181
184
|
|
|
182
185
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
183
186
|
|
|
184
187
|
describe("ENDPOINTS registry", () => {
|
|
185
|
-
test("has exactly
|
|
188
|
+
test("has exactly 15 endpoint entries", () => {
|
|
186
189
|
assert.equal(
|
|
187
190
|
ENDPOINT_KEYS.length,
|
|
188
191
|
EXPECTED_ENDPOINT_COUNT,
|
|
@@ -803,7 +806,7 @@ describe("GET /api/achievements", () => {
|
|
|
803
806
|
// matching its discriminator.
|
|
804
807
|
|
|
805
808
|
describe("SSE /api/events — SseEvent union discriminator validation", () => {
|
|
806
|
-
/** All
|
|
809
|
+
/** All 21 SseEvent variants with their discriminator `type` value and a minimal payload. */
|
|
807
810
|
const SSE_VARIANTS: ReadonlyArray<{
|
|
808
811
|
discriminator: string;
|
|
809
812
|
payload: string;
|
|
@@ -996,13 +999,23 @@ describe("SSE /api/events — SseEvent union discriminator validation", () => {
|
|
|
996
999
|
frame: "\x1b[2J\x1b[H",
|
|
997
1000
|
}),
|
|
998
1001
|
},
|
|
1002
|
+
{
|
|
1003
|
+
discriminator: "session_sample",
|
|
1004
|
+
payload: JSON.stringify({
|
|
1005
|
+
type: "session_sample",
|
|
1006
|
+
ts: "2025-01-01T00:00:00Z",
|
|
1007
|
+
sessionId: "sess-abc",
|
|
1008
|
+
tokens: 42_000,
|
|
1009
|
+
percent: 35.5,
|
|
1010
|
+
}),
|
|
1011
|
+
},
|
|
999
1012
|
];
|
|
1000
1013
|
|
|
1001
|
-
test("all
|
|
1014
|
+
test("all 21 SseEvent variants have a type field matching their discriminator", () => {
|
|
1002
1015
|
assert.equal(
|
|
1003
1016
|
SSE_VARIANTS.length,
|
|
1004
|
-
|
|
1005
|
-
"SseEvent union should have
|
|
1017
|
+
21,
|
|
1018
|
+
"SseEvent union should have 21 variants",
|
|
1006
1019
|
);
|
|
1007
1020
|
|
|
1008
1021
|
for (const variant of SSE_VARIANTS) {
|
|
@@ -770,6 +770,106 @@ export async function launchDashboardServer(
|
|
|
770
770
|
return;
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
// /api/sessions — S39: active pi sessions with latest token usage + heartbeat.
|
|
774
|
+
// GET returns {updatedAt, pruned, sessions[]} after pruning stale heartbeats.
|
|
775
|
+
// The dashboard server is a detached child with no MegaRuntime ref, so it
|
|
776
|
+
// reads session_heartbeats via a require()'d sqlite helper (same pattern as
|
|
777
|
+
// /api/achievements, /api/perf). Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
778
|
+
if (req.url?.startsWith("/api/sessions")) {
|
|
779
|
+
// Guard: /api/sessions/timeseries handled separately below.
|
|
780
|
+
if (req.url.startsWith("/api/sessions/timeseries")) {
|
|
781
|
+
// Fall through to the timeseries handler below.
|
|
782
|
+
} else {
|
|
783
|
+
const sReq = createRequire(import.meta.url);
|
|
784
|
+
const {
|
|
785
|
+
readActiveSessions,
|
|
786
|
+
pruneStaleSessions,
|
|
787
|
+
listRepoRegistry,
|
|
788
|
+
} = sReq(
|
|
789
|
+
"../../src/store/sqlite.js",
|
|
790
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
791
|
+
if (req.method !== "GET") {
|
|
792
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
793
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
const pruned = pruneStaleSessions(); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
798
|
+
const active = readActiveSessions();
|
|
799
|
+
const repos = listRepoRegistry();
|
|
800
|
+
const repoMap = new Map(repos.map((r) => [r.repoRoot, r]));
|
|
801
|
+
const sessions = active.map((s) => ({
|
|
802
|
+
pid: s.pid,
|
|
803
|
+
sessionId: s.sessionId,
|
|
804
|
+
repoRoot: s.repoRoot,
|
|
805
|
+
displayName: s.repoRoot
|
|
806
|
+
? (s.repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? s.repoRoot)
|
|
807
|
+
: (s.stateDir?.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"),
|
|
808
|
+
model: s.repoRoot ? (repoMap.get(s.repoRoot)?.modelName ?? null) : null,
|
|
809
|
+
tokens: s.tokens,
|
|
810
|
+
percent: s.percent,
|
|
811
|
+
ctxWindow: s.ctxWindow,
|
|
812
|
+
lastSeen: s.lastSeen,
|
|
813
|
+
stateDir: s.stateDir,
|
|
814
|
+
}));
|
|
815
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
816
|
+
res.end(
|
|
817
|
+
JSON.stringify({ updatedAt: new Date().toISOString(), pruned, sessions }),
|
|
818
|
+
);
|
|
819
|
+
} catch (e) {
|
|
820
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
821
|
+
res.end(
|
|
822
|
+
JSON.stringify({ error: "sessions_unavailable", detail: String(e) }),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// /api/sessions/timeseries — S39: stacked per-session token timeseries for
|
|
830
|
+
// the recharts memory graph. GET ?minutes=N (clamped [1,1440]) returns
|
|
831
|
+
// {updatedAt, windowMinutes, series[], totals[]} in recharts-ready shape.
|
|
832
|
+
// Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
833
|
+
if (req.url?.startsWith("/api/sessions/timeseries")) {
|
|
834
|
+
const tsReq = createRequire(import.meta.url);
|
|
835
|
+
const {
|
|
836
|
+
readSessionTimeseries,
|
|
837
|
+
pruneTokenSamples,
|
|
838
|
+
} = tsReq(
|
|
839
|
+
"../../src/store/sqlite.js",
|
|
840
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
841
|
+
if (req.method !== "GET") {
|
|
842
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
843
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const url = new URL(req.url, "http://x"); // guardrails-allow PREVENT-PI-004: localhost dashboard URL base (loopback-only)
|
|
848
|
+
let minutes = Number(url.searchParams.get("minutes") ?? "30");
|
|
849
|
+
if (!Number.isFinite(minutes) || minutes <= 0) minutes = 30;
|
|
850
|
+
minutes = Math.min(Math.max(minutes, 1), 1440);
|
|
851
|
+
const sinceTs = Date.now() - minutes * 60_000;
|
|
852
|
+
const pruneMs = Math.max(minutes * 60_000, 1_800_000);
|
|
853
|
+
pruneTokenSamples(pruneMs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
854
|
+
const result = readSessionTimeseries(sinceTs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
855
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
856
|
+
res.end(
|
|
857
|
+
JSON.stringify({
|
|
858
|
+
updatedAt: new Date().toISOString(),
|
|
859
|
+
windowMinutes: minutes,
|
|
860
|
+
series: result.series,
|
|
861
|
+
totals: result.totals,
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
} catch (e) {
|
|
865
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
866
|
+
res.end(
|
|
867
|
+
JSON.stringify({ error: "timeseries_unavailable", detail: String(e) }),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
773
873
|
// Fallback — serve the React client build (SPA route) or legacy dashboard.
|
|
774
874
|
// Non-/api/* GETs hit here: serve client assets if built, else inline HTML.
|
|
775
875
|
if (
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { epochIdFor } from "../../src/mirror/epoch.js";
|
|
22
22
|
import { autoCompactCheck } from "../../src/compact.js";
|
|
23
23
|
import { estimateSessionTokens } from "../../src/tokens.js";
|
|
24
|
-
import {
|
|
24
|
+
import type { MegaRuntime } from "../mega-runtime.js";
|
|
25
25
|
import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
|
|
26
26
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
|
|
27
27
|
import {
|
|
@@ -42,13 +42,19 @@ function toRawTranscriptRow(
|
|
|
42
42
|
epochId: string,
|
|
43
43
|
): RawTranscriptRow | null {
|
|
44
44
|
// Narrow to Message union (has content + timestamp).
|
|
45
|
-
const m = msg as {
|
|
45
|
+
const m = msg as {
|
|
46
|
+
role?: string;
|
|
47
|
+
content?: unknown;
|
|
48
|
+
timestamp?: number;
|
|
49
|
+
toolName?: string;
|
|
50
|
+
};
|
|
46
51
|
const content = m.content;
|
|
47
52
|
if (content == null || content === "") return null;
|
|
48
53
|
// Canonical form: sort object keys for deterministic hashing.
|
|
49
|
-
const contentBytes =
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
const contentBytes =
|
|
55
|
+
typeof content === "string"
|
|
56
|
+
? content
|
|
57
|
+
: JSON.stringify(content, Object.keys(content as object).sort());
|
|
52
58
|
const contentHash = createHash("sha256").update(contentBytes).digest("hex");
|
|
53
59
|
return {
|
|
54
60
|
contentHash,
|
|
@@ -85,21 +91,32 @@ export function registerContextHandler(
|
|
|
85
91
|
pi.on("context", async (event: ContextEvent, ctx: ExtensionContext) => {
|
|
86
92
|
const usage = ctx.getContextUsage();
|
|
87
93
|
const pct = usage?.percent;
|
|
94
|
+
const messages = event.messages;
|
|
88
95
|
// Always track context for the dashboard/widget, even when auto is off.
|
|
89
96
|
// (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
|
|
90
97
|
// null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
|
|
91
|
-
|
|
98
|
+
// S40 fix: fall back to estimateSessionTokens(view) when the provider
|
|
99
|
+
// doesn't report tokens (e.g. plexus / claude-mythos-5 via OpenRouter).
|
|
100
|
+
// Without this, lastCtxTokens is null -> appendTokenSample (S39) never
|
|
101
|
+
// fires -> Sessions chart + Overview per-repo stack + ContextGauge all
|
|
102
|
+
// show empty/zero. Compute view lazily only when the fallback is needed
|
|
103
|
+
// (at most one engineView call per context event; when auto is on and
|
|
104
|
+
// usage.tokens is present, view is computed once below via reuse).
|
|
105
|
+
const viewForFallback =
|
|
106
|
+
usage?.tokens == null ? runtime.engineView(messages) : null;
|
|
107
|
+
const currentTokens =
|
|
108
|
+
usage?.tokens ??
|
|
109
|
+
(viewForFallback != null
|
|
110
|
+
? estimateSessionTokens(viewForFallback)
|
|
111
|
+
: null) ??
|
|
112
|
+
Math.round(((pct ?? 0) / 100) * (usage?.contextWindow ?? 0));
|
|
113
|
+
runtime.lastCtxTokens = currentTokens ?? null;
|
|
92
114
|
runtime.lastCtxPercent = pct ?? null;
|
|
93
115
|
runtime.lastCtxWindow = usage?.contextWindow ?? 0;
|
|
94
116
|
runtime.snapshot(ctx);
|
|
95
117
|
if (!config.auto) return;
|
|
96
118
|
|
|
97
|
-
const
|
|
98
|
-
const view = runtime.engineView(messages);
|
|
99
|
-
const currentTokens =
|
|
100
|
-
usage?.tokens ??
|
|
101
|
-
estimateSessionTokens(view) ??
|
|
102
|
-
Math.round(((pct ?? 0) / 100) * (usage?.contextWindow ?? 0));
|
|
119
|
+
const view = viewForFallback ?? runtime.engineView(messages);
|
|
103
120
|
|
|
104
121
|
// S27 DB-mirror: append ALL incoming messages to raw_transcript.
|
|
105
122
|
// Runs BEFORE fast-gate so every message is captured, even if we
|
|
@@ -178,7 +195,7 @@ export function registerContextHandler(
|
|
|
178
195
|
pct != null && runtime.trimCache.ctxPct != null
|
|
179
196
|
? pct - runtime.trimCache.ctxPct >= RECOMPACT_PCT_DELTA
|
|
180
197
|
: currentTokens - (runtime.trimCache.ctxTokens ?? 0) >=
|
|
181
|
-
|
|
198
|
+
runtime.effectiveThreshold * 0.5;
|
|
182
199
|
if (!grewEnough) {
|
|
183
200
|
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized once by computeLiveTrimCut (src/boundary.ts) and replayed verbatim; the transcript only grows within an epoch (cache is cleared on durable truncation), so the preserved run still starts on a toolPair-safe index.
|
|
184
201
|
runtime.diagLiveTrimFires++; // trim view returned this call (replay counts as a fire)
|
|
@@ -186,7 +203,9 @@ export function registerContextHandler(
|
|
|
186
203
|
runtime.snapshot(ctx);
|
|
187
204
|
// v0.8.7: shallow-copy the cached summary so pi's transformContext can't
|
|
188
205
|
// mutate the shared reference across replays (audit P3).
|
|
189
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
messages: [{ ...runtime.trimCache.summaryAgentMsg }, ...recent],
|
|
208
|
+
};
|
|
190
209
|
}
|
|
191
210
|
// else: context grew enough → fall through to re-compact (cache is stale)
|
|
192
211
|
}
|
|
@@ -195,7 +214,10 @@ export function registerContextHandler(
|
|
|
195
214
|
// with how close we are to the model context limit. Null-safe: when the
|
|
196
215
|
// token-fallback path ran (pct unavailable) use the token-basis pressure
|
|
197
216
|
// (the same basis the runtime `pressure` getter uses for custom/no-window).
|
|
198
|
-
const pressure =
|
|
217
|
+
const pressure =
|
|
218
|
+
pct != null
|
|
219
|
+
? pressureFromPct(pct)
|
|
220
|
+
: pressureRatio(currentTokens, runtime.effectiveThreshold);
|
|
199
221
|
const ran = runCompact(pi, runtime, config, ctx, messages, {
|
|
200
222
|
compressionPressure: pressure,
|
|
201
223
|
});
|
|
@@ -269,9 +291,9 @@ export function registerContextHandler(
|
|
|
269
291
|
setTimeout(() => {
|
|
270
292
|
try {
|
|
271
293
|
if (runtime.rt.sessionId !== liveSid) return; // session reset
|
|
272
|
-
const since2 =
|
|
273
|
-
|
|
274
|
-
|
|
294
|
+
const since2 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
295
|
+
if (runtime.rt.lastNativeCompactAt !== stamp && since2 < cooldownMs)
|
|
296
|
+
return;
|
|
275
297
|
if (piCompactWouldNoop(ctx)) return;
|
|
276
298
|
ctx.compact({
|
|
277
299
|
customInstructions: undefined,
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
getCacheHitTokensSaved,
|
|
30
30
|
getGameState,
|
|
31
31
|
recordPerfSample,
|
|
32
|
+
recordSessionHeartbeat,
|
|
33
|
+
appendTokenSample,
|
|
32
34
|
type ModelSnapshot,
|
|
33
35
|
type GameState,
|
|
34
36
|
} from "../../src/store/sqlite.js";
|
|
@@ -575,6 +577,35 @@ export class MegaRuntime {
|
|
|
575
577
|
} as DashboardSnapshot);
|
|
576
578
|
const perfDiskMs = this.dashboard.lastWriteMs;
|
|
577
579
|
|
|
580
|
+
// S39: record a session heartbeat + token sample into the shared
|
|
581
|
+
// machine-wide index.sqlite so the dashboard can show a real-time
|
|
582
|
+
// stacked-memory graph across all active pi processes. Behind the
|
|
583
|
+
// material-change gate (this code only runs when sig changed). Non-fatal
|
|
584
|
+
// try/catch mirrors the recordPerfSample pattern below. Skip the token
|
|
585
|
+
// sample when lastCtxTokens is null (no context data yet).
|
|
586
|
+
try {
|
|
587
|
+
const repo = resolveRepoRoot(ctx?.cwd ?? this.currentStateDir) ?? this.currentStateDir;
|
|
588
|
+
recordSessionHeartbeat(
|
|
589
|
+
process.pid,
|
|
590
|
+
this.rt.sessionId,
|
|
591
|
+
repo,
|
|
592
|
+
this.currentStateDir,
|
|
593
|
+
this.lastCtxWindow || 0,
|
|
594
|
+
);
|
|
595
|
+
if (this.lastCtxTokens != null) {
|
|
596
|
+
appendTokenSample(
|
|
597
|
+
this.rt.sessionId,
|
|
598
|
+
repo,
|
|
599
|
+
this.lastCtxTokens,
|
|
600
|
+
this.lastCtxPercent ?? 0,
|
|
601
|
+
this.lastCtxWindow || 0,
|
|
602
|
+
join(this.currentStateDir, "events.log"),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
/* non-fatal: S39 monitoring must never block the snapshot path */
|
|
607
|
+
}
|
|
608
|
+
|
|
578
609
|
// Live stats widget above the editor
|
|
579
610
|
if (ctx) {
|
|
580
611
|
// ── gather widget data (computed per snapshot, rendered per frame) ────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.19",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-2-Clause",
|