pi-mega-compact 0.15.0 → 0.15.2
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/resolve-snapshot.js +20 -8
- package/dist/src/store/sqlite/global-sessions.js +40 -7
- package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js → AreaChart-D0hDLKC1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js.map → AreaChart-D0hDLKC1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js → BarChart-DSbTWZTQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js.map → BarChart-DSbTWZTQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js → CacheTab-Bgt2Wuar.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js.map → CacheTab-Bgt2Wuar.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js → EventsTab-CBb7O-6M.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js.map → EventsTab-CBb7O-6M.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js → HealthTab-BFMHwnlg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js.map → HealthTab-BFMHwnlg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js → MaintenanceTab-DxIQyeh-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js.map → MaintenanceTab-DxIQyeh-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js → MemoryMapTab-C576-nmM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js.map → MemoryMapTab-C576-nmM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js → MetricsTab-vk8Nqg7k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js.map → MetricsTab-vk8Nqg7k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-BVaddBS4.js +16 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-BVaddBS4.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js → ReposTab-CLCd-Gfd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js.map → ReposTab-CLCd-Gfd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js → SessionsTab-CV_qDKZB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js.map → SessionsTab-CV_qDKZB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js → SetupTab-BuIwk7LZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js.map → SetupTab-BuIwk7LZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js → TimeSavedCard-BS5-w4mP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js.map → TimeSavedCard-BS5-w4mP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js → TurnsTab-DhgjZmEQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js.map → TurnsTab-DhgjZmEQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js → WikiTab-CmOAaPnM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js.map → WikiTab-CmOAaPnM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js → button-DyV48WDX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js.map → button-DyV48WDX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js → card-CfCQ36pG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js.map → card-CfCQ36pG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js → generateCategoricalChart-CGlLggNu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js.map → generateCategoricalChart-CGlLggNu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-pmtEr3sb.js → index-tPP7N38D.js} +28 -28
- package/extensions/dashboard-client/dist/assets/index-tPP7N38D.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js → switch-7461qYip.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js.map → switch-7461qYip.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js → toggle-CHyUS1MH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js.map → toggle-CHyUS1MH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js → useSSE-CPIU9hax.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js.map → useSSE-CPIU9hax.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/package-lock.json +0 -39
- 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 +32 -16
- package/extensions/dashboard-server/resolve-snapshot.ts +22 -10
- 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
|
@@ -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
|
}
|