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.
Files changed (57) hide show
  1. package/dist/extensions/dashboard-server/resolve-snapshot.js +20 -8
  2. package/dist/src/store/sqlite/global-sessions.js +40 -7
  3. package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js → AreaChart-D0hDLKC1.js} +2 -2
  4. package/extensions/dashboard-client/dist/assets/{AreaChart-rJdaOUlj.js.map → AreaChart-D0hDLKC1.js.map} +1 -1
  5. package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js → BarChart-DSbTWZTQ.js} +2 -2
  6. package/extensions/dashboard-client/dist/assets/{BarChart-DOfc8me9.js.map → BarChart-DSbTWZTQ.js.map} +1 -1
  7. package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js → CacheTab-Bgt2Wuar.js} +2 -2
  8. package/extensions/dashboard-client/dist/assets/{CacheTab-EhqkEglo.js.map → CacheTab-Bgt2Wuar.js.map} +1 -1
  9. package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js → EventsTab-CBb7O-6M.js} +2 -2
  10. package/extensions/dashboard-client/dist/assets/{EventsTab-BUACwWXf.js.map → EventsTab-CBb7O-6M.js.map} +1 -1
  11. package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js → HealthTab-BFMHwnlg.js} +2 -2
  12. package/extensions/dashboard-client/dist/assets/{HealthTab-CDEYVRBb.js.map → HealthTab-BFMHwnlg.js.map} +1 -1
  13. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js → MaintenanceTab-DxIQyeh-.js} +2 -2
  14. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DtanIeCQ.js.map → MaintenanceTab-DxIQyeh-.js.map} +1 -1
  15. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js → MemoryMapTab-C576-nmM.js} +2 -2
  16. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BGEhL7OG.js.map → MemoryMapTab-C576-nmM.js.map} +1 -1
  17. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js → MetricsTab-vk8Nqg7k.js} +2 -2
  18. package/extensions/dashboard-client/dist/assets/{MetricsTab-Cktozn4s.js.map → MetricsTab-vk8Nqg7k.js.map} +1 -1
  19. package/extensions/dashboard-client/dist/assets/OverviewTab-BVaddBS4.js +16 -0
  20. package/extensions/dashboard-client/dist/assets/OverviewTab-BVaddBS4.js.map +1 -0
  21. package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js → ReposTab-CLCd-Gfd.js} +2 -2
  22. package/extensions/dashboard-client/dist/assets/{ReposTab-Bg72Ozdc.js.map → ReposTab-CLCd-Gfd.js.map} +1 -1
  23. package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js → SessionsTab-CV_qDKZB.js} +2 -2
  24. package/extensions/dashboard-client/dist/assets/{SessionsTab-BcTK9Lg6.js.map → SessionsTab-CV_qDKZB.js.map} +1 -1
  25. package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js → SetupTab-BuIwk7LZ.js} +2 -2
  26. package/extensions/dashboard-client/dist/assets/{SetupTab-Bt-Ru1xp.js.map → SetupTab-BuIwk7LZ.js.map} +1 -1
  27. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js → TimeSavedCard-BS5-w4mP.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ic_LjJ79.js.map → TimeSavedCard-BS5-w4mP.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js → TurnsTab-DhgjZmEQ.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{TurnsTab-D9AZTMgG.js.map → TurnsTab-DhgjZmEQ.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js → WikiTab-CmOAaPnM.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{WikiTab-hRcJPi_m.js.map → WikiTab-CmOAaPnM.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js → button-DyV48WDX.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{button-BdAtIbVu.js.map → button-DyV48WDX.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js → card-CfCQ36pG.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{card-BFYc_QrK.js.map → card-CfCQ36pG.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js → generateCategoricalChart-CGlLggNu.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BwzyqytT.js.map → generateCategoricalChart-CGlLggNu.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{index-pmtEr3sb.js → index-tPP7N38D.js} +28 -28
  40. package/extensions/dashboard-client/dist/assets/index-tPP7N38D.js.map +1 -0
  41. package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js → switch-7461qYip.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{switch-DEyGlFwJ.js.map → switch-7461qYip.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js → toggle-CHyUS1MH.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{toggle-pDMBQYQj.js.map → toggle-CHyUS1MH.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js → useSSE-CPIU9hax.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{useSSE-BKaC4dao.js.map → useSSE-CPIU9hax.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/index.html +1 -1
  48. package/extensions/dashboard-client/package-lock.json +0 -39
  49. package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +43 -19
  50. package/extensions/dashboard-client/src/hooks/useApi.ts +38 -5
  51. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +32 -16
  52. package/extensions/dashboard-server/resolve-snapshot.ts +22 -10
  53. package/package.json +1 -1
  54. package/src/store/sqlite/global-sessions.ts +205 -175
  55. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js +0 -16
  56. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js.map +0 -1
  57. package/extensions/dashboard-client/dist/assets/index-pmtEr3sb.js.map +0 -1
@@ -19,7 +19,12 @@
19
19
 
20
20
  import type React from "react";
21
21
  import type { ActiveSession, SessionsResponse } from "@contracts";
22
- import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
22
+ import {
23
+ Card,
24
+ CardContent,
25
+ CardHeader,
26
+ CardTitle,
27
+ } from "../components/ui/card";
23
28
 
24
29
  export interface SessionContextGaugesProps {
25
30
  /** Active sessions from /api/sessions (may be null while loading). */
@@ -107,7 +112,11 @@ function SessionGauge({ data }: { data: GaugeData }): React.ReactElement {
107
112
  <div className="gauge-fill" style={{ width: `${fillWidth}%` }} />
108
113
  </div>
109
114
  <p className="gauge-label session-gauge-sub">{sublabel}</p>
110
- {data.sublabel && <p className="session-gauge-meta truncate" title={data.sublabel}>{data.sublabel}</p>}
115
+ {data.sublabel && (
116
+ <p className="session-gauge-meta truncate" title={data.sublabel}>
117
+ {data.sublabel}
118
+ </p>
119
+ )}
111
120
  </div>
112
121
  );
113
122
  }
@@ -135,11 +144,26 @@ export function SessionContextGauges({
135
144
  ];
136
145
 
137
146
  // Other active sessions (exclude the launcher by sessionId to avoid dup).
138
- const others: ActiveSession[] = (sessions?.sessions ?? []).filter(
147
+ // Also dedup BY PID: the heartbeat schema PK is (pid, session_id), so a
148
+ // process that restarted into a new session_id leaves a stale
149
+ // (pid, old_session_id) row. That stale row has a different sessionId so
150
+ // the launcher filter above doesn't catch it, and it surfaces as a
151
+ // phantom 0% box (null tokens). Keep only the most-recently-seen row per
152
+ // pid (rows are sorted by lastSeen DESC below before the loop).
153
+ const othersRaw: ActiveSession[] = (sessions?.sessions ?? []).filter(
139
154
  (s) => s.sessionId !== launcherSessionId,
140
155
  );
141
- // Sort by lastSeen descending (most recent first).
142
- others.sort((a, b) => b.lastSeen - a.lastSeen);
156
+ othersRaw.sort((a, b) => b.lastSeen - a.lastSeen);
157
+ const seenPid = new Set<number>();
158
+ const others: ActiveSession[] = othersRaw.filter((s) => {
159
+ // Skip stale rows with no token sample — they have nothing to gauge
160
+ // and are always either a fresh session (briefly) or a dead lingering
161
+ // row. Either way a 0% / "?" gauge is noise.
162
+ if (s.tokens == null && s.percent == null) return false;
163
+ if (seenPid.has(s.pid)) return false;
164
+ seenPid.add(s.pid);
165
+ return true;
166
+ });
143
167
  for (const s of others) {
144
168
  gauges.push({
145
169
  key: s.sessionId,
@@ -163,21 +187,21 @@ export function SessionContextGauges({
163
187
  <CardTitle>Context per Session (live)</CardTitle>
164
188
  </CardHeader>
165
189
  <CardContent>
166
- {hasError ? (
167
- <div className="text-sm text-muted-foreground">
168
- Error loading sessions: {error!.message}
169
- </div>
170
- ) : showLoading ? (
171
- <div className="text-sm text-muted-foreground">Loading sessions…</div>
172
- ) : (
173
- <div className="session-gauges-scroll">
174
- <div className="session-gauges-grid">
175
- {gauges.map((g) => (
176
- <SessionGauge key={g.key} data={g} />
177
- ))}
190
+ {hasError ? (
191
+ <div className="text-sm text-muted-foreground">
192
+ Error loading sessions: {error!.message}
178
193
  </div>
179
- </div>
180
- )}
194
+ ) : showLoading ? (
195
+ <div className="text-sm text-muted-foreground">Loading sessions…</div>
196
+ ) : (
197
+ <div className="session-gauges-scroll">
198
+ <div className="session-gauges-grid">
199
+ {gauges.map((g) => (
200
+ <SessionGauge key={g.key} data={g} />
201
+ ))}
202
+ </div>
203
+ </div>
204
+ )}
181
205
  </CardContent>
182
206
  </Card>
183
207
  );
@@ -24,15 +24,45 @@ export interface UseApiOptions {
24
24
  retryBaseMs?: number;
25
25
  }
26
26
 
27
- // SPRINT-D1-REMAINING: integrate retryWithBackoff from utils/retry.ts.
28
- // SPRINT-D1-REMAINING: integrate staleness detection from utils/staleness.ts.
27
+ // SPRINT-D1: retry with exponential backoff on fetch failure, so a
28
+ // transient network blip (dashboard server still starting, ECONNRESET, a
29
+ // single malformed response) auto-recovers instead of surfacing a hard
30
+ // error screen that forces a manual page refresh. Crosses any fetch this
31
+ // hook backs (snapshot, sessions, ...).
32
+ function sleep(ms: number): Promise<void> {
33
+ return new Promise((r) => setTimeout(r, ms));
34
+ }
35
+
36
+ /** Fetch with bounded exponential backoff retry. The retry schedule is
37
+ * {0, retryBaseMs, retryBaseMs*2, retryBaseMs*4, ...} capped at retryBaseMs*8,
38
+ * so maxRetries=3 yields ~0ms + 500ms + 1000ms + 2000ms before giving up. */
39
+ async function fetchWithRetry<T>(
40
+ fetchFn: () => Promise<T>,
41
+ maxRetries: number,
42
+ retryBaseMs: number,
43
+ ): Promise<T> {
44
+ let lastErr: unknown;
45
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
46
+ try {
47
+ return await fetchFn();
48
+ } catch (err) {
49
+ lastErr = err;
50
+ if (attempt === maxRetries) break;
51
+ // Exponential backoff capped at 8x base to avoid long stalls.
52
+ const delay = Math.min(retryBaseMs * 2 ** attempt, retryBaseMs * 8);
53
+ await sleep(delay);
54
+ }
55
+ }
56
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
57
+ }
58
+
29
59
  // SPRINT-T1-REMAINING: add Authorization header when auth token present.
30
60
 
31
61
  export function useApi<T>(
32
62
  fetchFn: () => Promise<T>,
33
63
  options: UseApiOptions = {},
34
64
  ): UseApiResult<T> {
35
- const { pollInterval = 0 } = options;
65
+ const { pollInterval = 0, maxRetries = 3, retryBaseMs = 500 } = options;
36
66
  const [data, setData] = useState<T | null>(null);
37
67
  const [error, setError] = useState<Error | null>(null);
38
68
  const [loading, setLoading] = useState(true);
@@ -43,7 +73,10 @@ export function useApi<T>(
43
73
  setLoading(true);
44
74
  setError(null);
45
75
  try {
46
- const result = await fetchFn();
76
+ // Retry transient failures with exponential backoff so a single
77
+ // network blip on page load auto-recovers instead of showing a
78
+ // hard error that requires a manual refresh.
79
+ const result = await fetchWithRetry(fetchFn, maxRetries, retryBaseMs);
47
80
  if (mountedRef.current) {
48
81
  setData(result);
49
82
  setLastFetchedAt(Date.now());
@@ -57,7 +90,7 @@ export function useApi<T>(
57
90
  setLoading(false);
58
91
  }
59
92
  }
60
- }, [fetchFn]);
93
+ }, [fetchFn, maxRetries, retryBaseMs]);
61
94
 
62
95
  useEffect(() => {
63
96
  mountedRef.current = true;
@@ -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<Array<{ ts: number; value: number }>>([]);
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(() => {
@@ -177,7 +177,15 @@ export default function OverviewTab({
177
177
  if (loading && !snapshot)
178
178
  return <div className="tab-stub">Loading snapshot…</div>;
179
179
  if (error && !snapshot)
180
- return <div className="tab-stub">Error: {error.message}</div>;
180
+ return (
181
+ <div className="tab-stub">
182
+ <p>Error loading snapshot: {error.message}</p>
183
+ <p className="text-xs text-muted-foreground mt-2">
184
+ Auto-retried with backoff and will re-poll every 5s — no action
185
+ needed.
186
+ </p>
187
+ </div>
188
+ );
181
189
  if (!snapshot) return <div className="tab-stub">No snapshot data.</div>;
182
190
 
183
191
  // Cast to RuntimeSnapshot for tierPct (runtime-only field).
@@ -389,15 +397,15 @@ export default function OverviewTab({
389
397
  onClose={() => setOpenWidget(null)}
390
398
  >
391
399
  {perfLoading ? (
392
- <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
400
+ <p className="py-8 text-center text-sm text-muted-foreground">
401
+ Loading…
402
+ </p>
393
403
  ) : perfError ? (
394
- <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
404
+ <p className="py-8 text-center text-sm text-red-400">
405
+ {perfError.message}
406
+ </p>
395
407
  ) : (
396
- <PerfLineChart
397
- data={perfData}
398
- label="Cache hit %"
399
- color="#58a6ff"
400
- />
408
+ <PerfLineChart data={perfData} label="Cache hit %" color="#58a6ff" />
401
409
  )}
402
410
  </WidgetDetailModal>
403
411
 
@@ -407,9 +415,13 @@ export default function OverviewTab({
407
415
  onClose={() => setOpenWidget(null)}
408
416
  >
409
417
  {perfLoading ? (
410
- <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
418
+ <p className="py-8 text-center text-sm text-muted-foreground">
419
+ Loading…
420
+ </p>
411
421
  ) : perfError ? (
412
- <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
422
+ <p className="py-8 text-center text-sm text-red-400">
423
+ {perfError.message}
424
+ </p>
413
425
  ) : (
414
426
  <PerfLineChart
415
427
  data={perfData}
@@ -425,9 +437,13 @@ export default function OverviewTab({
425
437
  onClose={() => setOpenWidget(null)}
426
438
  >
427
439
  {perfLoading ? (
428
- <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
440
+ <p className="py-8 text-center text-sm text-muted-foreground">
441
+ Loading…
442
+ </p>
429
443
  ) : perfError ? (
430
- <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
444
+ <p className="py-8 text-center text-sm text-red-400">
445
+ {perfError.message}
446
+ </p>
431
447
  ) : (
432
448
  <PerfLineChart
433
449
  data={perfData}
@@ -5,11 +5,17 @@
5
5
  * active pi session may be running in a different repo. This module queries
6
6
  * the repo registry to find the most recently active repo's dashboard.json,
7
7
  * so the Overview always shows live data from the repo pi is actually running
8
- * in — not the stale launcher dir.
8
+ * in.
9
+ *
10
+ * The launch stateDir is a candidate like any other (NOT excluded): when
11
+ * `/dashboard` was run in the repo you are still coding in, the launch dir
12
+ * IS the most-recently-active repo and must win. Excluding it would serve a
13
+ * stale other-repo snapshot, surfacing a phantom 0% "this session" gauge
14
+ * plus a duplicate live-session gauge.
9
15
  *
10
16
  * Resolution order:
11
- * 1. Most recently seen repo (by last_seen in repo_registry) whose
12
- * dashboard.json exists and has a non-null updatedAt
17
+ * 1. Most recently seen repo (by last_seen in repo_registry, launch dir
18
+ * included) whose dashboard.json exists and has a non-null updatedAt
13
19
  * 2. The static launch stateDir's dashboard.json (original behavior)
14
20
  */
15
21
 
@@ -35,23 +41,30 @@ interface RepoRegistryRow {
35
41
 
36
42
  /**
37
43
  * Find the most recently active repo's stateDir by scanning the repo registry
38
- * for repos whose dashboard.json exists and was written recently. Returns null
39
- * if no suitable repo is found.
44
+ * for the repo with the highest last_seen whose dashboard.json exists and has
45
+ * a non-null updatedAt. Returns null if no suitable repo is found.
46
+ *
47
+ * The launch stateDir is NOT excluded: if it is itself the most-recently-active
48
+ * repo (the common case — `/dashboard` was run in the repo you are still
49
+ * coding in), its live dashboard.json must be served. Excluding it would cause
50
+ * a stale *other*-repo snapshot to win, producing a phantom 0% "this session"
51
+ * gauge + a duplicate live-session gauge.
40
52
  */
41
- function mostRecentRepoStateDir(staticStateDir: string): string | null {
53
+ function mostRecentRepoStateDir(_staticStateDir: string): string | null {
42
54
  const indexPath = join(getIndexDir(), "index.sqlite");
43
55
  if (!existsSync(indexPath)) return null;
44
56
  let db;
45
57
  try {
46
- const { DatabaseSync } = require("node:sqlite") as typeof import("node:sqlite");
58
+ const { DatabaseSync } =
59
+ require("node:sqlite") as typeof import("node:sqlite");
47
60
  db = new DatabaseSync(indexPath, { readOnly: true });
48
61
  const rows = db
49
62
  .prepare(
50
63
  `SELECT state_dir, last_seen FROM repo_registry
51
- WHERE state_dir IS NOT NULL AND state_dir != ?
64
+ WHERE state_dir IS NOT NULL AND last_seen IS NOT NULL
52
65
  ORDER BY last_seen DESC`,
53
66
  )
54
- .all(staticStateDir) as unknown as RepoRegistryRow[];
67
+ .all() as unknown as RepoRegistryRow[];
55
68
  for (const r of rows) {
56
69
  const dashPath = join(r.state_dir, "dashboard.json");
57
70
  if (!existsSync(dashPath)) continue;
@@ -95,4 +108,3 @@ export function resolveSnapshot(
95
108
  stateDir: staticStateDir,
96
109
  };
97
110
  }
98
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
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",