pi-mega-compact 0.15.1 → 0.15.3
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/src/store/sqlite/global-sessions.js +40 -7
- package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js → AreaChart-CMri0vM1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js.map → AreaChart-CMri0vM1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js → BarChart-BbkxxSkM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js.map → BarChart-BbkxxSkM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js → CacheTab-CiHS6tod.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js.map → CacheTab-CiHS6tod.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js → EventsTab-NnD7EpRm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js.map → EventsTab-NnD7EpRm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js → HealthTab-Dlt7WTh7.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js.map → HealthTab-Dlt7WTh7.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js → MaintenanceTab-DgKBZmdD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js.map → MaintenanceTab-DgKBZmdD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js → MemoryMapTab-SwTOzuZW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js.map → MemoryMapTab-SwTOzuZW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js → MetricsTab-CWEgBTx0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js.map → MetricsTab-CWEgBTx0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-B0YDSSMH.js +16 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-B0YDSSMH.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js → ReposTab-CMKZ_a4B.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js.map → ReposTab-CMKZ_a4B.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js → SessionsTab-D2cCDKB3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js.map → SessionsTab-D2cCDKB3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js → SetupTab-HUAG7xDf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js.map → SetupTab-HUAG7xDf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js → TimeSavedCard-NFCsyJhB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js.map → TimeSavedCard-NFCsyJhB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js → TurnsTab-BY7_AZaG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js.map → TurnsTab-BY7_AZaG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js → WikiTab-MbmjqKVQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js.map → WikiTab-MbmjqKVQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js → button-CoVmU9Vr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js.map → button-CoVmU9Vr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js → card-Cp9Hpdoo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js.map → card-Cp9Hpdoo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js → generateCategoricalChart-CMM8OUO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js.map → generateCategoricalChart-CMM8OUO5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-pmtEr3sb.js → index-CRHmWnRi.js} +28 -28
- package/extensions/dashboard-client/dist/assets/index-CRHmWnRi.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js → switch-jrYrJJYy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js.map → switch-jrYrJJYy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js → toggle-DT1AN5wS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js.map → toggle-DT1AN5wS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js → useSSE-rJH_Rx6K.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js.map → useSSE-rJH_Rx6K.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +43 -19
- package/extensions/dashboard-client/src/hooks/useApi.ts +38 -5
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +52 -27
- package/package.json +1 -1
- package/src/store/sqlite/global-sessions.ts +205 -175
- package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js +0 -16
- package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-pmtEr3sb.js.map +0 -1
|
@@ -124,7 +124,9 @@ export default function OverviewTab({
|
|
|
124
124
|
// which widget's modal is open (null = none)
|
|
125
125
|
const [openWidget, setOpenWidget] = useState<string | null>(null);
|
|
126
126
|
// perf samples fetched for the opened perf-backed widget
|
|
127
|
-
const [perfData, setPerfData] = useState<
|
|
127
|
+
const [perfData, setPerfData] = useState<
|
|
128
|
+
Array<{ ts: number; value: number }>
|
|
129
|
+
>([]);
|
|
128
130
|
const [perfLoading, setPerfLoading] = useState(false);
|
|
129
131
|
const [perfError, setPerfError] = useState<Error | null>(null);
|
|
130
132
|
|
|
@@ -148,9 +150,7 @@ export default function OverviewTab({
|
|
|
148
150
|
})
|
|
149
151
|
.catch((err) => {
|
|
150
152
|
if (!cancelled) {
|
|
151
|
-
setPerfError(
|
|
152
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
153
|
-
);
|
|
153
|
+
setPerfError(err instanceof Error ? err : new Error(String(err)));
|
|
154
154
|
}
|
|
155
155
|
})
|
|
156
156
|
.finally(() => {
|
|
@@ -174,10 +174,38 @@ export default function OverviewTab({
|
|
|
174
174
|
[moveCard],
|
|
175
175
|
);
|
|
176
176
|
|
|
177
|
+
// Per-model cost breakdown for the Model & Cost drill-down, derived from
|
|
178
|
+
// snapshot data already in the component (no snapshot.byModel exists).
|
|
179
|
+
// MUST be called before the early returns below — Rules of Hooks: hooks
|
|
180
|
+
// cannot be after a conditional return (would cause React error #310
|
|
181
|
+
// "Rendered more hooks than during the previous render" when the snapshot
|
|
182
|
+
// transitions from null → loaded). Guard the null case inline.
|
|
183
|
+
const modelBarData = useMemo(
|
|
184
|
+
() => {
|
|
185
|
+
const ds = snapshot as RuntimeSnapshot | null;
|
|
186
|
+
const compression = ds?.compression;
|
|
187
|
+
const model = ds?.model;
|
|
188
|
+
return [
|
|
189
|
+
{ label: "Tokens freed", value: compression?.repo.tokensFreed ?? 0 },
|
|
190
|
+
{ label: "Input rate", value: model ? model.inputRate : 0 },
|
|
191
|
+
{ label: "Output rate", value: model ? model.outputRate : 0 },
|
|
192
|
+
];
|
|
193
|
+
},
|
|
194
|
+
[snapshot],
|
|
195
|
+
);
|
|
196
|
+
|
|
177
197
|
if (loading && !snapshot)
|
|
178
198
|
return <div className="tab-stub">Loading snapshot…</div>;
|
|
179
199
|
if (error && !snapshot)
|
|
180
|
-
return
|
|
200
|
+
return (
|
|
201
|
+
<div className="tab-stub">
|
|
202
|
+
<p>Error loading snapshot: {error.message}</p>
|
|
203
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
204
|
+
Auto-retried with backoff and will re-poll every 5s — no action
|
|
205
|
+
needed.
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
181
209
|
if (!snapshot) return <div className="tab-stub">No snapshot data.</div>;
|
|
182
210
|
|
|
183
211
|
// Cast to RuntimeSnapshot for tierPct (runtime-only field).
|
|
@@ -192,17 +220,6 @@ export default function OverviewTab({
|
|
|
192
220
|
const repo = d.repo;
|
|
193
221
|
const model = d.model;
|
|
194
222
|
|
|
195
|
-
// Per-model cost breakdown for the Model & Cost drill-down, derived from
|
|
196
|
-
// snapshot data already in the component (no snapshot.byModel exists).
|
|
197
|
-
const modelBarData = useMemo(
|
|
198
|
-
() => [
|
|
199
|
-
{ label: "Tokens freed", value: compression.repo.tokensFreed },
|
|
200
|
-
{ label: "Input rate", value: model ? model.inputRate : 0 },
|
|
201
|
-
{ label: "Output rate", value: model ? model.outputRate : 0 },
|
|
202
|
-
],
|
|
203
|
-
[compression, model],
|
|
204
|
-
);
|
|
205
|
-
|
|
206
223
|
// Renders a single card (wrapped in SortableCard) by its CardId. Cards
|
|
207
224
|
// with a drill-down get an onClick; the rest are plain sortable items.
|
|
208
225
|
const renderCard = (cardId: CardId): React.ReactNode => {
|
|
@@ -389,15 +406,15 @@ export default function OverviewTab({
|
|
|
389
406
|
onClose={() => setOpenWidget(null)}
|
|
390
407
|
>
|
|
391
408
|
{perfLoading ? (
|
|
392
|
-
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
409
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
410
|
+
Loading…
|
|
411
|
+
</p>
|
|
393
412
|
) : perfError ? (
|
|
394
|
-
<p className="py-8 text-center text-sm text-red-400">
|
|
413
|
+
<p className="py-8 text-center text-sm text-red-400">
|
|
414
|
+
{perfError.message}
|
|
415
|
+
</p>
|
|
395
416
|
) : (
|
|
396
|
-
<PerfLineChart
|
|
397
|
-
data={perfData}
|
|
398
|
-
label="Cache hit %"
|
|
399
|
-
color="#58a6ff"
|
|
400
|
-
/>
|
|
417
|
+
<PerfLineChart data={perfData} label="Cache hit %" color="#58a6ff" />
|
|
401
418
|
)}
|
|
402
419
|
</WidgetDetailModal>
|
|
403
420
|
|
|
@@ -407,9 +424,13 @@ export default function OverviewTab({
|
|
|
407
424
|
onClose={() => setOpenWidget(null)}
|
|
408
425
|
>
|
|
409
426
|
{perfLoading ? (
|
|
410
|
-
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
427
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
428
|
+
Loading…
|
|
429
|
+
</p>
|
|
411
430
|
) : perfError ? (
|
|
412
|
-
<p className="py-8 text-center text-sm text-red-400">
|
|
431
|
+
<p className="py-8 text-center text-sm text-red-400">
|
|
432
|
+
{perfError.message}
|
|
433
|
+
</p>
|
|
413
434
|
) : (
|
|
414
435
|
<PerfLineChart
|
|
415
436
|
data={perfData}
|
|
@@ -425,9 +446,13 @@ export default function OverviewTab({
|
|
|
425
446
|
onClose={() => setOpenWidget(null)}
|
|
426
447
|
>
|
|
427
448
|
{perfLoading ? (
|
|
428
|
-
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
449
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
450
|
+
Loading…
|
|
451
|
+
</p>
|
|
429
452
|
) : perfError ? (
|
|
430
|
-
<p className="py-8 text-center text-sm text-red-400">
|
|
453
|
+
<p className="py-8 text-center text-sm text-red-400">
|
|
454
|
+
{perfError.message}
|
|
455
|
+
</p>
|
|
431
456
|
) : (
|
|
432
457
|
<PerfLineChart
|
|
433
458
|
data={perfData}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
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-3-Clause",
|
|
@@ -18,56 +18,56 @@ import { openIndexStore, getIndexDir } from "./global-index.js";
|
|
|
18
18
|
|
|
19
19
|
/** A live active session row (joined heartbeat + latest token sample). */
|
|
20
20
|
export interface ActiveSessionRow {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
pid: number;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
repoRoot: string | null;
|
|
24
|
+
stateDir: string | null;
|
|
25
|
+
ctxWindow: number;
|
|
26
|
+
lastSeen: number;
|
|
27
|
+
tokens: number | null;
|
|
28
|
+
percent: number | null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** A single time-series data point for a session. */
|
|
32
32
|
export interface TokenSamplePoint {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
ts: number;
|
|
34
|
+
tokens: number;
|
|
35
|
+
percent: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** A recharts-ready per-session series with a stable color. */
|
|
39
39
|
export interface SessionSeries {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
sessionId: string;
|
|
41
|
+
label: string;
|
|
42
|
+
color: string;
|
|
43
|
+
data: TokenSamplePoint[];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Timeseries response shape (recharts-ready). */
|
|
47
47
|
export interface SessionTimeseriesResult {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
series: SessionSeries[];
|
|
49
|
+
totals: { ts: number; tokens: number }[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** Stable color palette for per-session series (hash-based, no randomness). */
|
|
53
53
|
const SESSION_COLORS = [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
"#60a5fa", // blue-400
|
|
55
|
+
"#34d399", // emerald-400
|
|
56
|
+
"#fbbf24", // amber-400
|
|
57
|
+
"#f87171", // red-400
|
|
58
|
+
"#a78bfa", // violet-400
|
|
59
|
+
"#f472b6", // pink-400
|
|
60
|
+
"#22d3ee", // cyan-400
|
|
61
|
+
"#a3e635", // lime-400
|
|
62
62
|
];
|
|
63
63
|
|
|
64
64
|
/** Hash a sessionId to a stable color index. */
|
|
65
65
|
function sessionColor(sessionId: string): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
let h = 0;
|
|
67
|
+
for (let i = 0; i < sessionId.length; i++) {
|
|
68
|
+
h = (h * 31 + sessionId.charCodeAt(i)) | 0;
|
|
69
|
+
}
|
|
70
|
+
return SESSION_COLORS[Math.abs(h) % SESSION_COLORS.length];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -76,31 +76,42 @@ function sessionColor(sessionId: string): string {
|
|
|
76
76
|
* their own row. Non-fatal on conflict (INSERT ... ON CONFLICT DO UPDATE).
|
|
77
77
|
*/
|
|
78
78
|
export function recordSessionHeartbeat(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
pid: number,
|
|
80
|
+
sessionId: string,
|
|
81
|
+
repoRoot: string,
|
|
82
|
+
stateDir: string,
|
|
83
|
+
ctxWindow: number,
|
|
84
|
+
indexDir: string = getIndexDir(),
|
|
85
85
|
): void {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const db = openIndexStore(indexDir);
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
db.prepare(
|
|
89
|
+
`INSERT INTO session_heartbeats (pid, session_id, repo_root, state_dir, ctx_window, last_seen)
|
|
90
90
|
VALUES (@pid, @session_id, @repo_root, @state_dir, @ctx_window, @last_seen)
|
|
91
91
|
ON CONFLICT(pid, session_id) DO UPDATE SET
|
|
92
92
|
repo_root = excluded.repo_root,
|
|
93
93
|
state_dir = excluded.state_dir,
|
|
94
94
|
ctx_window = excluded.ctx_window,
|
|
95
95
|
last_seen = excluded.last_seen`,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
).run({
|
|
97
|
+
pid,
|
|
98
|
+
session_id: sessionId,
|
|
99
|
+
repo_root: repoRoot,
|
|
100
|
+
state_dir: stateDir,
|
|
101
|
+
ctx_window: ctxWindow,
|
|
102
|
+
last_seen: now,
|
|
103
|
+
});
|
|
104
|
+
// Enforce the invariant: one heartbeat row per live pi PROCESS (pid).
|
|
105
|
+
// A pi process owns exactly one session_id at a time — when it restarts /
|
|
106
|
+
// resumes into a new session_id, the OLD (pid, old_session_id) row is stale
|
|
107
|
+
// and must be deleted, otherwise it lingers (its last_seen was updated by
|
|
108
|
+
// the prior heartbeat, so the 30-min prune won't catch it) and surfaces as
|
|
109
|
+
// a phantom 0% "pi-<repo>" gauge in the dashboard's "Context per Session"
|
|
110
|
+
// card (null tokens, dead session_id that no longer matches the launcher).
|
|
111
|
+
// Parameterized (PREVENT-002).
|
|
112
|
+
db.prepare(
|
|
113
|
+
`DELETE FROM session_heartbeats WHERE pid = @pid AND session_id != @session_id`,
|
|
114
|
+
).run({ pid, session_id: sessionId });
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
/**
|
|
@@ -109,54 +120,56 @@ export function recordSessionHeartbeat(
|
|
|
109
120
|
* callers without an events.log (e.g. tests) can omit it.
|
|
110
121
|
*/
|
|
111
122
|
export function appendTokenSample(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
sessionId: string,
|
|
124
|
+
repoRoot: string,
|
|
125
|
+
tokens: number,
|
|
126
|
+
percent: number,
|
|
127
|
+
ctxWindow: number,
|
|
128
|
+
eventsLogPath: string | null,
|
|
129
|
+
indexDir: string = getIndexDir(),
|
|
119
130
|
): void {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
const db = openIndexStore(indexDir);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
db.prepare(
|
|
134
|
+
`INSERT INTO token_samples (session_id, repo_root, tokens, percent, ctx_window, ts)
|
|
124
135
|
VALUES (@session_id, @repo_root, @tokens, @percent, @ctx_window, @ts)`,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
136
|
+
).run({
|
|
137
|
+
session_id: sessionId,
|
|
138
|
+
repo_root: repoRoot,
|
|
139
|
+
tokens,
|
|
140
|
+
percent,
|
|
141
|
+
ctx_window: ctxWindow,
|
|
142
|
+
ts: now,
|
|
143
|
+
});
|
|
144
|
+
// Step 5: also append a session_sample JSON line to events.log so the
|
|
145
|
+
// existing /api/events SSE tail streams it for free (real-time chart push).
|
|
146
|
+
// Mirrors the DashboardEmitter events.log append pattern in
|
|
147
|
+
// extensions/mega-dashboard.ts:{ event(type, data) }: a JSON object with
|
|
148
|
+
// `ts` (ISO 8601 string), `type`, and the event-specific payload. The ISO
|
|
149
|
+
// timestamp matches the shape of every other SSE variant (SseSessionSample
|
|
150
|
+
// contract; every SSE variant's `ts` is a string); a numeric ms `ts` would
|
|
151
|
+
// violate the contract union's "every SSE variant has a ts field of type
|
|
152
|
+
// string" invariant and break DashboardEmitter consumers.
|
|
153
|
+
if (eventsLogPath) {
|
|
154
|
+
try {
|
|
155
|
+
const dir = eventsLogPath.includes("/")
|
|
156
|
+
? eventsLogPath.slice(0, eventsLogPath.lastIndexOf("/"))
|
|
157
|
+
: ".";
|
|
158
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
159
|
+
appendFileSync(
|
|
160
|
+
eventsLogPath,
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
ts: new Date(now).toISOString(),
|
|
163
|
+
type: "session_sample",
|
|
164
|
+
sessionId,
|
|
165
|
+
tokens,
|
|
166
|
+
percent,
|
|
167
|
+
}) + "\n",
|
|
168
|
+
);
|
|
169
|
+
} catch {
|
|
170
|
+
/* non-fatal: SSE push is best-effort */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
/**
|
|
@@ -164,15 +177,15 @@ export function appendTokenSample(
|
|
|
164
177
|
* Default 30-min retention. Called by /api/sessions.
|
|
165
178
|
*/
|
|
166
179
|
export function pruneStaleSessions(
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
maxAgeMs: number = 1_800_000,
|
|
181
|
+
indexDir: string = getIndexDir(),
|
|
169
182
|
): number {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
const db = openIndexStore(indexDir);
|
|
184
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
185
|
+
const result = db
|
|
186
|
+
.prepare("DELETE FROM session_heartbeats WHERE last_seen < @cutoff")
|
|
187
|
+
.run({ cutoff });
|
|
188
|
+
return Number(result.changes);
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
/**
|
|
@@ -180,15 +193,15 @@ export function pruneStaleSessions(
|
|
|
180
193
|
* Called by /api/sessions/timeseries.
|
|
181
194
|
*/
|
|
182
195
|
export function pruneTokenSamples(
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
maxAgeMs: number = 1_800_000,
|
|
197
|
+
indexDir: string = getIndexDir(),
|
|
185
198
|
): number {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
const db = openIndexStore(indexDir);
|
|
200
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
201
|
+
const result = db
|
|
202
|
+
.prepare("DELETE FROM token_samples WHERE ts < @cutoff")
|
|
203
|
+
.run({ cutoff });
|
|
204
|
+
return Number(result.changes);
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
@@ -197,11 +210,12 @@ export function pruneTokenSamples(
|
|
|
197
210
|
* via a correlated subquery. Returns rows sorted by last_seen descending.
|
|
198
211
|
*/
|
|
199
212
|
export function readActiveSessions(
|
|
200
|
-
|
|
213
|
+
indexDir: string = getIndexDir(),
|
|
201
214
|
): ActiveSessionRow[] {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
const db = openIndexStore(indexDir);
|
|
216
|
+
const rows = db
|
|
217
|
+
.prepare(
|
|
218
|
+
`SELECT h.pid, h.session_id, h.repo_root, h.state_dir, h.ctx_window, h.last_seen,
|
|
205
219
|
s.tokens, s.percent
|
|
206
220
|
FROM session_heartbeats h
|
|
207
221
|
LEFT JOIN token_samples s ON s.id = (
|
|
@@ -210,26 +224,40 @@ export function readActiveSessions(
|
|
|
210
224
|
ORDER BY t.ts DESC LIMIT 1
|
|
211
225
|
)
|
|
212
226
|
ORDER BY h.last_seen DESC`,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
)
|
|
228
|
+
.all() as Array<{
|
|
229
|
+
pid: number;
|
|
230
|
+
session_id: string;
|
|
231
|
+
repo_root: string | null;
|
|
232
|
+
state_dir: string | null;
|
|
233
|
+
ctx_window: number;
|
|
234
|
+
last_seen: number;
|
|
235
|
+
tokens: number | null;
|
|
236
|
+
percent: number | null;
|
|
237
|
+
}>;
|
|
238
|
+
// Dedup BY PID (one row per live pi process, keeping the most-recently-seen
|
|
239
|
+
// row). The schema PK is (pid, session_id), so a process that restarted /
|
|
240
|
+
// resumed into a new session_id leaves a stale (pid, old_session_id) row
|
|
241
|
+
// until recordSessionHeartbeat's cleanup fires or the 30-min prune runs.
|
|
242
|
+
// Without this dedup those stale rows surface as phantom 0% gauges in the
|
|
243
|
+
// "Context per Session" card (null tokens, dead session_id). Rows are
|
|
244
|
+
// already ordered by last_seen DESC, so the first row seen for a pid wins.
|
|
245
|
+
const seenPid = new Set<number>();
|
|
246
|
+
const deduped = rows.filter((r) => {
|
|
247
|
+
if (seenPid.has(r.pid)) return false;
|
|
248
|
+
seenPid.add(r.pid);
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
return deduped.map((r) => ({
|
|
252
|
+
pid: r.pid,
|
|
253
|
+
sessionId: r.session_id,
|
|
254
|
+
repoRoot: r.repo_root,
|
|
255
|
+
stateDir: r.state_dir,
|
|
256
|
+
ctxWindow: r.ctx_window ?? 0,
|
|
257
|
+
lastSeen: r.last_seen,
|
|
258
|
+
tokens: r.tokens,
|
|
259
|
+
percent: r.percent,
|
|
260
|
+
}));
|
|
233
261
|
}
|
|
234
262
|
|
|
235
263
|
/**
|
|
@@ -238,41 +266,43 @@ export function readActiveSessions(
|
|
|
238
266
|
* [{ts, tokens}] (sum of all sessions at each timestamp).
|
|
239
267
|
*/
|
|
240
268
|
export function readSessionTimeseries(
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
sinceMs: number,
|
|
270
|
+
indexDir: string = getIndexDir(),
|
|
243
271
|
): SessionTimeseriesResult {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
const db = openIndexStore(indexDir);
|
|
273
|
+
const rows = db
|
|
274
|
+
.prepare(
|
|
275
|
+
`SELECT session_id, tokens, percent, ts FROM token_samples WHERE ts >= @since ORDER BY ts ASC`,
|
|
276
|
+
)
|
|
277
|
+
.all({ since: sinceMs }) as Array<{
|
|
278
|
+
session_id: string;
|
|
279
|
+
tokens: number;
|
|
280
|
+
percent: number;
|
|
281
|
+
ts: number;
|
|
282
|
+
}>;
|
|
283
|
+
// Group by session_id → series; + accumulate totals per timestamp.
|
|
284
|
+
const seriesMap = new Map<string, TokenSamplePoint[]>();
|
|
285
|
+
const totalsMap = new Map<number, number>();
|
|
286
|
+
for (const r of rows) {
|
|
287
|
+
let pts = seriesMap.get(r.session_id);
|
|
288
|
+
if (!pts) {
|
|
289
|
+
pts = [];
|
|
290
|
+
seriesMap.set(r.session_id, pts);
|
|
291
|
+
}
|
|
292
|
+
pts.push({ ts: r.ts, tokens: r.tokens, percent: r.percent });
|
|
293
|
+
totalsMap.set(r.ts, (totalsMap.get(r.ts) ?? 0) + r.tokens);
|
|
294
|
+
}
|
|
295
|
+
const series: SessionSeries[] = [];
|
|
296
|
+
for (const [sessionId, data] of seriesMap) {
|
|
297
|
+
const label = sessionId.length > 12 ? sessionId.slice(0, 12) : sessionId;
|
|
298
|
+
series.push({ sessionId, label, color: sessionColor(sessionId), data });
|
|
299
|
+
}
|
|
300
|
+
// Sort series by first-timestamp for stable legend order.
|
|
301
|
+
series.sort((a, b) => (a.data[0]?.ts ?? 0) - (b.data[0]?.ts ?? 0));
|
|
302
|
+
const totals = Array.from(totalsMap.entries())
|
|
303
|
+
.sort((a, b) => a[0] - b[0])
|
|
304
|
+
.map(([ts, tokens]) => ({ ts, tokens }));
|
|
305
|
+
return { series, totals };
|
|
276
306
|
}
|
|
277
307
|
|
|
278
308
|
/**
|
|
@@ -280,12 +310,12 @@ export function readSessionTimeseries(
|
|
|
280
310
|
* Non-fatal: no-op if the row doesn't exist.
|
|
281
311
|
*/
|
|
282
312
|
export function clearSessionHeartbeat(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
313
|
+
pid: number,
|
|
314
|
+
sessionId: string,
|
|
315
|
+
indexDir: string = getIndexDir(),
|
|
286
316
|
): void {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
317
|
+
const db = openIndexStore(indexDir);
|
|
318
|
+
db.prepare(
|
|
319
|
+
"DELETE FROM session_heartbeats WHERE pid = @pid AND session_id = @session_id",
|
|
320
|
+
).run({ pid, session_id: sessionId });
|
|
291
321
|
}
|