pi-mega-compact 0.8.20 → 0.8.21

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 (38) hide show
  1. package/README.md +6 -0
  2. package/dist/extensions/dashboard-client/src/hooks/useApi.js +51 -0
  3. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +63 -0
  4. package/dist/extensions/mega-compact-s38.test.js +28 -2
  5. package/dist/extensions/mega-events/agent-handlers.js +16 -0
  6. package/dist/extensions/mega-events/error-classifier.js +8 -3
  7. package/extensions/dashboard-client/package-lock.json +1 -1
  8. package/extensions/dashboard-client/package.json +1 -1
  9. package/extensions/mega-compact-s38.test.ts +32 -2
  10. package/extensions/mega-events/agent-handlers.ts +15 -0
  11. package/extensions/mega-events/error-classifier.ts +8 -2
  12. package/package.json +1 -1
  13. package/dist/extensions/dashboard-server/helpers.js +0 -37
  14. package/dist/extensions/dashboard-server/html/all-repos-tab.js +0 -26
  15. package/dist/extensions/dashboard-server/html/body-open.js +0 -23
  16. package/dist/extensions/dashboard-server/html/current-repo-tab.js +0 -130
  17. package/dist/extensions/dashboard-server/html/head-open.js +0 -16
  18. package/dist/extensions/dashboard-server/html/high-score-tab.js +0 -25
  19. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +0 -26
  20. package/dist/extensions/dashboard-server/html/script.js +0 -259
  21. package/dist/extensions/dashboard-server/html/styles.js +0 -103
  22. package/dist/extensions/dashboard-server/html/summary-tab.js +0 -19
  23. package/dist/extensions/dashboard-server/html-template.js +0 -41
  24. package/dist/src/store/sqlite/connection.js +0 -35
  25. package/dist/src/store/sqlite/index-store.js +0 -167
  26. package/dist/src/store/sqlite/memory.js +0 -54
  27. package/dist/src/store/sqlite/minhash-lsh.js +0 -47
  28. package/dist/src/store/sqlite/sessions.js +0 -39
  29. package/dist/src/store/sqlite/transaction.js +0 -19
  30. package/dist/src/vectorStore/add.js +0 -260
  31. package/dist/src/vectorStore/dedup.js +0 -52
  32. package/dist/src/vectorStore/index.js +0 -10
  33. package/dist/src/vectorStore/queries.js +0 -83
  34. package/dist/src/vectorStore/search.js +0 -95
  35. package/dist/src/vectorStore/session.js +0 -19
  36. package/dist/src/vectorStore/store.js +0 -105
  37. package/dist/src/vectorStore/types.js +0 -6
  38. package/dist/src/vectorStore/utils.js +0 -23
package/README.md CHANGED
@@ -89,3 +89,9 @@ Testing guide: [`TESTER_GUIDE.md`](TESTER_GUIDE.md)
89
89
  ## License
90
90
 
91
91
  BSD-2-Clause
92
+
93
+ ## ☕ Support
94
+
95
+ If this project helped you, consider buying me a coffee:
96
+
97
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-TheArchitectit-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/TheArchitectit)
@@ -0,0 +1,51 @@
1
+ /**
2
+ * dashboard-client/src/hooks/useApi.ts — Generic data fetching hook.
3
+ *
4
+ * Provides typed fetch with retry, stale detection, and error handling.
5
+ * SPRINT-B1: basic fetch. SPRINT-D1: retry + stale.
6
+ */
7
+ import { useState, useEffect, useCallback, useRef } from 'react';
8
+ // SPRINT-D1-REMAINING: integrate retryWithBackoff from utils/retry.ts.
9
+ // SPRINT-D1-REMAINING: integrate staleness detection from utils/staleness.ts.
10
+ // SPRINT-T1-REMAINING: add Authorization header when auth token present.
11
+ export function useApi(fetchFn, options = {}) {
12
+ const { pollInterval = 0, maxRetries = 0 } = options;
13
+ const [data, setData] = useState(null);
14
+ const [error, setError] = useState(null);
15
+ const [loading, setLoading] = useState(true);
16
+ const [lastFetchedAt, setLastFetchedAt] = useState(null);
17
+ const mountedRef = useRef(true);
18
+ const doFetch = useCallback(async () => {
19
+ setLoading(true);
20
+ setError(null);
21
+ try {
22
+ const result = await fetchFn();
23
+ if (mountedRef.current) {
24
+ setData(result);
25
+ setLastFetchedAt(Date.now());
26
+ }
27
+ }
28
+ catch (err) {
29
+ if (mountedRef.current) {
30
+ setError(err instanceof Error ? err : new Error(String(err)));
31
+ }
32
+ }
33
+ finally {
34
+ if (mountedRef.current) {
35
+ setLoading(false);
36
+ }
37
+ }
38
+ }, [fetchFn]);
39
+ useEffect(() => {
40
+ mountedRef.current = true;
41
+ doFetch();
42
+ return () => { mountedRef.current = false; };
43
+ }, [doFetch]);
44
+ useEffect(() => {
45
+ if (pollInterval <= 0)
46
+ return;
47
+ const timer = setInterval(doFetch, pollInterval);
48
+ return () => clearInterval(timer);
49
+ }, [doFetch, pollInterval]);
50
+ return { data, error, loading, refetch: doFetch, lastFetchedAt };
51
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.
3
+ *
4
+ * Connects to /api/events, provides real-time event stream.
5
+ * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.
6
+ */
7
+ import { useState, useEffect, useRef, useCallback } from 'react';
8
+ // SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).
9
+ // SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.
10
+ export function useSSE(options = {}) {
11
+ const { maxEvents = 500, maxReconnects = 5 } = options;
12
+ const [events, setEvents] = useState([]);
13
+ const [status, setStatus] = useState('connecting');
14
+ const [eventCount, setEventCount] = useState(0);
15
+ const [lastEventAt, setLastEventAt] = useState(null);
16
+ const reconnectCount = useRef(0);
17
+ const sourceRef = useRef(null);
18
+ const connect = useCallback(() => {
19
+ if (sourceRef.current) {
20
+ sourceRef.current.close();
21
+ }
22
+ setStatus('connecting');
23
+ const source = new EventSource('/api/events');
24
+ sourceRef.current = source;
25
+ source.onopen = () => {
26
+ setStatus('connected');
27
+ reconnectCount.current = 0;
28
+ };
29
+ source.onmessage = (event) => {
30
+ try {
31
+ const parsed = JSON.parse(event.data);
32
+ setEvents(prev => {
33
+ const next = [...prev, parsed];
34
+ return next.length > maxEvents ? next.slice(-maxEvents) : next;
35
+ });
36
+ setEventCount(c => c + 1);
37
+ setLastEventAt(Date.now());
38
+ }
39
+ catch {
40
+ // non-fatal: skip malformed events
41
+ }
42
+ };
43
+ source.onerror = () => {
44
+ source.close();
45
+ if (reconnectCount.current < maxReconnects) {
46
+ reconnectCount.current += 1;
47
+ setStatus('connecting');
48
+ // SPRINT-D1: use exponential backoff instead of fixed delay
49
+ setTimeout(connect, 1000);
50
+ }
51
+ else {
52
+ setStatus('disconnected');
53
+ }
54
+ };
55
+ }, [maxEvents, maxReconnects]);
56
+ useEffect(() => {
57
+ connect();
58
+ return () => {
59
+ sourceRef.current?.close();
60
+ };
61
+ }, [connect]);
62
+ return { events, status, eventCount, lastEventAt };
63
+ }
@@ -122,9 +122,19 @@ async function s38TurnEnd(h, stopReason, text) {
122
122
  await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message }, lowCtx);
123
123
  }
124
124
  // ---- classifier unit tests (no extension harness needed) ----
125
- test("S38: classifyError returns 'transient' for error/aborted stopReasons", () => {
125
+ test("S38: classifyError returns 'transient' for error stopReason", () => {
126
126
  assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
127
- assert.equal(classifyErrorFn({ stopReason: "aborted" }), "transient");
127
+ });
128
+ test("S38: classifyError returns 'cancelled' for aborted stopReason (ESC/Ctrl-C)", () => {
129
+ assert.equal(classifyErrorFn({ stopReason: "aborted" }), "cancelled");
130
+ assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Operation aborted" }), "cancelled");
131
+ assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Aborted after 3 retry attempts" }), "cancelled");
132
+ });
133
+ test("S38: classifyError does NOT return 'cancelled' for error message containing 'aborted' with non-aborted stopReason", () => {
134
+ // Defense: the old text-based s.includes('aborted') path is gone,
135
+ // but verify that an error message mentioning 'aborted' with stopReason
136
+ // 'error' (not 'aborted') still classifies as transient, not cancelled.
137
+ assert.equal(classifyErrorFn({ stopReason: "error", errorMessage: "Connection aborted" }), "transient");
128
138
  });
129
139
  test("S38: classifyError returns 'transient' for max-output-token text", () => {
130
140
  assert.equal(classifyErrorFn({ stopReason: "error", content: "reached the maximum output token limit" }), "transient");
@@ -212,6 +222,22 @@ test("S38: context-overflow fires NO blind retry nudge and logs 'context_overflo
212
222
  assert.equal(h.sendUserMessages.length, 0, "context-overflow: NO blind retry nudge fired");
213
223
  assert.ok(ev.includes("context_overflow"), "context-overflow: 'context_overflow' event logged");
214
224
  });
225
+ test("S38: ESC-abort (stopReason='aborted') fires NO retry nudge and logs 'error_retry_cancelled'", async () => {
226
+ const h = harness();
227
+ await s38TurnEnd(h, "aborted", "Operation aborted");
228
+ const ev = eventTypes(h.stateDir);
229
+ assert.equal(h.sendUserMessages.length, 0, "cancelled: NO retry nudge fired after ESC abort");
230
+ assert.ok(ev.includes("error_retry_cancelled"), "cancelled: 'error_retry_cancelled' event logged");
231
+ });
232
+ test("S38: ESC-abort resets errorRetryCount and consecutiveErrors so next transient retry starts fresh", async () => {
233
+ const h = harness();
234
+ // First: simulate an ESC abort (should reset both counters to 0)
235
+ await s38TurnEnd(h, "aborted", "Operation aborted");
236
+ assert.equal(h.sendUserMessages.length, 0, "cancelled: no nudge on abort");
237
+ // Then: simulate a real transient error — should fire a fresh retry from count=1
238
+ await s38TurnEnd(h, "error", "500 Internal Server Error");
239
+ assert.equal(h.sendUserMessages.length, 1, "transient after cancel: retry fires from fresh count");
240
+ });
215
241
  // ---- integration tests (fire turn_end through the real extension) ----
216
242
  test("S38: compaction-noop logs 'compaction_noop_diagnostic' + resets counter + no retry fired", async () => {
217
243
  const h = harness();
@@ -313,6 +313,22 @@ export function registerAgentHandlers(pi, runtime, config) {
313
313
  turnIndex: event.turnIndex,
314
314
  });
315
315
  }
316
+ else if (category === 'cancelled') {
317
+ // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
318
+ // nudging would restart a task the user explicitly stopped.
319
+ // Reset both counters (a cancel is not an error for circuit-breaker
320
+ // purposes). Emit a diagnostic so the dashboard shows it.
321
+ runtime.rt.errorRetryCount = 0;
322
+ runtime.rt.consecutiveErrors = 0;
323
+ runtime.dashboard.event('error_retry_cancelled', {
324
+ turnIndex: event.turnIndex,
325
+ sessionId: runtime.rt.sessionId,
326
+ });
327
+ runtime.logger.info('error-retry-cancelled', {
328
+ sessionId: runtime.rt.sessionId,
329
+ turnIndex: event.turnIndex,
330
+ });
331
+ }
316
332
  else if (category === 'context-overflow') {
317
333
  // (4b) context-window overflow 400 ("too long... even after compaction").
318
334
  // NOT a blind retry: re-submitting the same oversized prompt would just
@@ -11,7 +11,7 @@
11
11
  * exclusively (its agent_end nudge path is separate and must not be doubled).
12
12
  *
13
13
  * @param message the event.message (a pi AgentMessage) or an error string
14
- * @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | null (success/unknown)
14
+ * @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | 'cancelled' | null (success/unknown)
15
15
  */
16
16
  export function classifyError(message) {
17
17
  // Resolve a searchable text blob from a pi AgentMessage or raw string.
@@ -25,6 +25,10 @@ export function classifyError(message) {
25
25
  // S28 guard: length stopReason is handled exclusively by the S28 path.
26
26
  if (sr === 'length')
27
27
  return null;
28
+ // User ESC / Ctrl-C abort — stopReason === 'aborted'. Not retryable:
29
+ // nudging would restart a task the user explicitly stopped.
30
+ if (sr === 'aborted')
31
+ return 'cancelled';
28
32
  // Success / normal tool flow — not an error, nothing to retry.
29
33
  if (sr === 'stop' || sr === 'toolUse' || sr === 'tool_use')
30
34
  return null;
@@ -111,8 +115,9 @@ export function classifyError(message) {
111
115
  if (s.includes('error') && !/\b(permanent|invalid request|malformed|bad request|auth|unauthorized|invalid (api )?key|permission)\b/.test(s)) {
112
116
  return 'transient'; // generic pi stopReason 'error' / 'aborted'
113
117
  }
114
- if (s.includes('aborted'))
115
- return 'transient';
118
+ // NOTE: 'aborted' stopReason is handled by the sr==='aborted' early-return above.
119
+ // The text-based s.includes('aborted') was removed — it was the old path that
120
+ // misclassified user ESC/Ctrl-C as 'transient' (5 retry nudges after cancel).
116
121
  if (/max(imum)? output token/.test(s))
117
122
  return 'transient';
118
123
  if (/rate[\s.-]?limit|429|too many requests/.test(s))
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "react": "^18.3.1",
12
12
  "react-dom": "^18.3.1",
13
- "recharts": "^2.13.0"
13
+ "recharts": "^2.15.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/react": "^18.3.12",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "react": "^18.3.1",
15
15
  "react-dom": "^18.3.1",
16
- "recharts": "^2.13.0"
16
+ "recharts": "^2.15.4"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/react": "^18.3.12",
@@ -121,9 +121,21 @@ async function s38TurnEnd(h: ReturnType<typeof harness>, stopReason: string | un
121
121
 
122
122
  // ---- classifier unit tests (no extension harness needed) ----
123
123
 
124
- test("S38: classifyError returns 'transient' for error/aborted stopReasons", () => {
124
+ test("S38: classifyError returns 'transient' for error stopReason", () => {
125
125
  assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
126
- assert.equal(classifyErrorFn({ stopReason: "aborted" }), "transient");
126
+ });
127
+
128
+ test("S38: classifyError returns 'cancelled' for aborted stopReason (ESC/Ctrl-C)", () => {
129
+ assert.equal(classifyErrorFn({ stopReason: "aborted" }), "cancelled");
130
+ assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Operation aborted" }), "cancelled");
131
+ assert.equal(classifyErrorFn({ stopReason: "aborted", errorMessage: "Aborted after 3 retry attempts" }), "cancelled");
132
+ });
133
+
134
+ test("S38: classifyError does NOT return 'cancelled' for error message containing 'aborted' with non-aborted stopReason", () => {
135
+ // Defense: the old text-based s.includes('aborted') path is gone,
136
+ // but verify that an error message mentioning 'aborted' with stopReason
137
+ // 'error' (not 'aborted') still classifies as transient, not cancelled.
138
+ assert.equal(classifyErrorFn({ stopReason: "error", errorMessage: "Connection aborted" }), "transient");
127
139
  });
128
140
 
129
141
  test("S38: classifyError returns 'transient' for max-output-token text", () => {
@@ -254,6 +266,24 @@ test("S38: context-overflow fires NO blind retry nudge and logs 'context_overflo
254
266
  assert.ok(ev.includes("context_overflow"), "context-overflow: 'context_overflow' event logged");
255
267
  });
256
268
 
269
+ test("S38: ESC-abort (stopReason='aborted') fires NO retry nudge and logs 'error_retry_cancelled'", async () => {
270
+ const h = harness();
271
+ await s38TurnEnd(h, "aborted", "Operation aborted");
272
+ const ev = eventTypes(h.stateDir);
273
+ assert.equal(h.sendUserMessages.length, 0, "cancelled: NO retry nudge fired after ESC abort");
274
+ assert.ok(ev.includes("error_retry_cancelled"), "cancelled: 'error_retry_cancelled' event logged");
275
+ });
276
+
277
+ test("S38: ESC-abort resets errorRetryCount and consecutiveErrors so next transient retry starts fresh", async () => {
278
+ const h = harness();
279
+ // First: simulate an ESC abort (should reset both counters to 0)
280
+ await s38TurnEnd(h, "aborted", "Operation aborted");
281
+ assert.equal(h.sendUserMessages.length, 0, "cancelled: no nudge on abort");
282
+ // Then: simulate a real transient error — should fire a fresh retry from count=1
283
+ await s38TurnEnd(h, "error", "500 Internal Server Error");
284
+ assert.equal(h.sendUserMessages.length, 1, "transient after cancel: retry fires from fresh count");
285
+ });
286
+
257
287
  // ---- integration tests (fire turn_end through the real extension) ----
258
288
 
259
289
  test("S38: compaction-noop logs 'compaction_noop_diagnostic' + resets counter + no retry fired", async () => {
@@ -347,6 +347,21 @@ export function registerAgentHandlers(
347
347
  sessionId: runtime.rt.sessionId,
348
348
  turnIndex: event.turnIndex,
349
349
  });
350
+ } else if (category === 'cancelled') {
351
+ // (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
352
+ // nudging would restart a task the user explicitly stopped.
353
+ // Reset both counters (a cancel is not an error for circuit-breaker
354
+ // purposes). Emit a diagnostic so the dashboard shows it.
355
+ runtime.rt.errorRetryCount = 0;
356
+ runtime.rt.consecutiveErrors = 0;
357
+ runtime.dashboard.event('error_retry_cancelled', {
358
+ turnIndex: event.turnIndex,
359
+ sessionId: runtime.rt.sessionId,
360
+ });
361
+ runtime.logger.info('error-retry-cancelled', {
362
+ sessionId: runtime.rt.sessionId,
363
+ turnIndex: event.turnIndex,
364
+ });
350
365
  } else if (category === 'context-overflow') {
351
366
  // (4b) context-window overflow 400 ("too long... even after compaction").
352
367
  // NOT a blind retry: re-submitting the same oversized prompt would just
@@ -12,13 +12,14 @@
12
12
  * exclusively (its agent_end nudge path is separate and must not be doubled).
13
13
  *
14
14
  * @param message the event.message (a pi AgentMessage) or an error string
15
- * @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | null (success/unknown)
15
+ * @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | 'cancelled' | null (success/unknown)
16
16
  */
17
17
  export function classifyError(message: unknown):
18
18
  | 'transient'
19
19
  | 'permanent'
20
20
  | 'compaction-noop'
21
21
  | 'context-overflow'
22
+ | 'cancelled'
22
23
  | null {
23
24
  // Resolve a searchable text blob from a pi AgentMessage or raw string.
24
25
  let text = '';
@@ -33,6 +34,9 @@ export function classifyError(message: unknown):
33
34
  const sr = typeof m.stopReason === 'string' ? m.stopReason : '';
34
35
  // S28 guard: length stopReason is handled exclusively by the S28 path.
35
36
  if (sr === 'length') return null;
37
+ // User ESC / Ctrl-C abort — stopReason === 'aborted'. Not retryable:
38
+ // nudging would restart a task the user explicitly stopped.
39
+ if (sr === 'aborted') return 'cancelled';
36
40
  // Success / normal tool flow — not an error, nothing to retry.
37
41
  if (sr === 'stop' || sr === 'toolUse' || sr === 'tool_use') return null;
38
42
  const parts: string[] = [];
@@ -109,7 +113,9 @@ export function classifyError(message: unknown):
109
113
  if (s.includes('error') && !/\b(permanent|invalid request|malformed|bad request|auth|unauthorized|invalid (api )?key|permission)\b/.test(s)) {
110
114
  return 'transient'; // generic pi stopReason 'error' / 'aborted'
111
115
  }
112
- if (s.includes('aborted')) return 'transient';
116
+ // NOTE: 'aborted' stopReason is handled by the sr==='aborted' early-return above.
117
+ // The text-based s.includes('aborted') was removed — it was the old path that
118
+ // misclassified user ESC/Ctrl-C as 'transient' (5 retry nudges after cancel).
113
119
  if (/max(imum)? output token/.test(s)) return 'transient';
114
120
  if (/rate[\s.-]?limit|429|too many requests/.test(s)) return 'transient';
115
121
  if (/5\d\d|internal server|bad gateway|service unavailable/.test(s)) return 'transient';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
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",
@@ -1,37 +0,0 @@
1
- /**
2
- * File-reading helper functions for the dashboard server.
3
- */
4
- import { readFileSync } from "node:fs";
5
- export function readSnapshot(snapshotPath) {
6
- try {
7
- const raw = readFileSync(snapshotPath, "utf-8");
8
- return JSON.parse(raw);
9
- }
10
- catch {
11
- return {
12
- version: 1,
13
- updatedAt: null,
14
- tier: "unknown",
15
- config: { fastGatePct: 80, thresholdTokens: 100_000, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
16
- session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
17
- context: { tokens: null, percent: null, contextWindow: 0 },
18
- trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80 },
19
- store: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
20
- crew: { activeAgents: 0, currentTurn: 0 },
21
- repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
22
- integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
23
- model: undefined,
24
- };
25
- }
26
- }
27
- export function readFrom(path, charOffset) {
28
- try {
29
- const content = readFileSync(path, "utf-8");
30
- if (content.length <= charOffset)
31
- return { data: "", offset: charOffset };
32
- return { data: content.slice(charOffset), offset: content.length };
33
- }
34
- catch {
35
- return { data: "", offset: charOffset };
36
- }
37
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * "All repos" tab panel — machine-wide registry table (index.sqlite).
3
- *
4
- * Rows are populated by the index poller in `script.ts` (shared with the
5
- * in-panel table on the Current-repo tab via the same render call).
6
- */
7
- export function allReposTab() {
8
- return `<!-- All repos (machine-wide registry from index.sqlite) -->
9
- <div class="tab-panel" id="panel-all">
10
- <table class="repos">
11
- <thead>
12
- <tr>
13
- <th>Repo</th><th>Model</th>
14
- <th style="text-align:right">Checkpoints</th>
15
- <th style="text-align:right">Tokens Saved</th>
16
- <th style="text-align:right">Retained</th>
17
- <th style="text-align:right">Last Compacted</th>
18
- </tr>
19
- </thead>
20
- <tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
21
- </table>
22
- <div class="updated" id="all-updated"></div>
23
- </div>
24
-
25
- `;
26
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Body opening: </head><body>, offline banner, page heading, and the tab nav.
3
- *
4
- * `tierName` is interpolated into the heading tier badge.
5
- * Includes the new "High Score" future-tab button (next project).
6
- */
7
- export function bodyOpen(tierName) {
8
- return `</head>
9
- <body>
10
-
11
- <div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
12
-
13
- <h1><span>mega-compact</span><span class="tier">${tierName}</span><span class="model-pill" id="hdr-model">—</span></h1>
14
-
15
- <nav class="tabs">
16
- <button class="tab active" data-tab="current">Current repo</button>
17
- <button class="tab" data-tab="all">All repos</button>
18
- <button class="tab" data-tab="summary">Summary</button>
19
- <button class="tab future" data-tab="highscore">High Score<span class="soon">soon</span></button>
20
- </nav>
21
-
22
- `;
23
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * "Current repo" tab panel — the per-repo dashboard view.
3
- *
4
- * Contains: context-window meter, trigger status, vector-store stats,
5
- * repo-wide stats, data-safety card, configuration, model & cost savings,
6
- * crew/agents, a legend, the live event stream, and the in-panel all-repos
7
- * table (shared rows with the All-repos tab via the index poller).
8
- *
9
- * `tierName` is interpolated into the Configuration card.
10
- */
11
- export function currentRepoTab(tierName) {
12
- return `<!-- Current repo (existing single-repo view) -->
13
- <div class="tab-panel" id="panel-current">
14
- <div class="grid">
15
- <div class="card">
16
- <h2>Context Window</h2>
17
- <div class="meter-label" id="ctx-pct">—</div>
18
- <div class="meter-track"><div class="meter-fill" id="ctx-bar" style="width:0%"></div></div>
19
- <div class="meter-sub" id="ctx-sub">waiting for data</div>
20
- </div>
21
- <div class="card">
22
- <h2>Trigger Status</h2>
23
- <div class="status-row"><div class="bullet" id="tr-armed"></div><span>Armed (context ≥ fast gate)</span></div>
24
- <div class="status-row"><div class="bullet" id="tr-ready"></div><span>Ready (tokens ≥ threshold)</span></div>
25
- <div class="state-text" id="tr-state">waiting</div>
26
- </div>
27
- <div class="card">
28
- <h2>Vector Store</h2>
29
- <div class="stat-grid">
30
- <span class="label" title="A saved summary of a chunk of your conversation that was compacted to free up space.">Checkpoints</span><span class="value" id="st-count">0</span>
31
- <span class="label" title="How much conversation we are currently holding as compact summaries (the 'memory' this extension keeps). Smaller is better.">Tokens Stored</span><span class="value" id="st-tokens">0</span>
32
- <span class="label" title="Total size of the original conversation text before it was compacted.">Original Tokens</span><span class="value" id="st-orig">0</span>
33
- <span class="label" title="How much conversation space we have freed up for you (original size minus the compact summary we kept).">Tokens Saved</span><span class="value" id="st-saved">0</span>
34
- <span class="label" title="How many times old context was automatically brought back into the conversation because it was relevant to what you were doing.">Injected</span><span class="value" id="st-injected">0</span>
35
- <span class="label" title="Of the times we recalled old context, how often it was actually on-topic.">Recall Relevance</span><span class="value" id="st-dedup">0%</span>
36
- <span class="label" title="How often new content matched something we already had, so we skipped storing a duplicate copy. Higher = less wasted space.">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
37
- <span class="label" title="How many duplicate chunks we collapsed into one instead of storing separately.">Collapsed</span><span class="value" id="st-collapsed">0</span>
38
- <span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
39
- </div>
40
- </div>
41
- <div class="card">
42
- <h2>Repo (all sessions)</h2>
43
- <div class="stat-grid">
44
- <span class="label">Checkpoints</span><span class="value" id="rp-count">0</span>
45
- <span class="label">Tokens Stored</span><span class="value" id="rp-tokens">0</span>
46
- <span class="label">Original Tokens</span><span class="value" id="rp-orig">0</span>
47
- <span class="label">Tokens Saved</span><span class="value" id="rp-saved">0</span>
48
- <span class="label">Sessions</span><span class="value" id="rp-sessions">0</span>
49
- <span class="label">Collapsed</span><span class="value" id="rp-collapsed">0</span>
50
- <span class="label">Storage Dedup</span><span class="value" id="rp-sdedup">0%</span>
51
- </div>
52
- </div>
53
- <div class="card safe">
54
- <h2>🛡 Data Safety</h2>
55
- <div class="stat-grid">
56
- <span class="label">Regions Retained</span><span class="value" id="ig-retained">0</span>
57
- <span class="label">Compressed-Original</span><span class="value" id="ig-bytes">0 B</span>
58
- <span class="label">Dedup Duplicates</span><span class="value" id="ig-dupes">0</span>
59
- <span class="label">Permanently Deleted</span><span class="value ok" id="ig-deleted">0 B</span>
60
- </div>
61
- <p class="safe-note">Every compacted region is kept verbatim (compressed). "Drop" = removed from the live window only. We never delete your data.</p>
62
- </div>
63
- <div class="card">
64
- <h2>Configuration</h2>
65
- <div class="conf-grid">
66
- <span class="label">Tier</span><span class="value" id="cf-tier">${tierName}</span>
67
- <span class="label">Threshold</span><span class="value" id="cf-threshold">—</span>
68
- <span class="label">Fast Gate</span><span class="value" id="cf-gate">—</span>
69
- <span class="label">Auto</span><span class="value" id="cf-auto">—</span>
70
- <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
71
- </div>
72
- </div>
73
- <div class="card cost">
74
- <h2>💰 Model &amp; Cost Savings</h2>
75
- <div class="cost-usd" id="cost-usd">≈ $0.00 saved</div>
76
- <div class="cost-sub" id="cost-windows">0 context-windows extended</div>
77
- <div class="stat-grid" style="margin-top:12px">
78
- <span class="label" title="The model pi is currently using — its pricing drives the cost figure.">Model</span><span class="value" id="md-name">—</span>
79
- <span class="label" title="The provider serving the model.">Provider</span><span class="value" id="md-provider">—</span>
80
- <span class="label" title="USD per input token, from the model's pricing.">Input Rate</span><span class="value" id="md-input">—</span>
81
- <span class="label" title="USD per output token, from the model's pricing.">Output Rate</span><span class="value" id="md-output">—</span>
82
- </div>
83
- </div>
84
- <div class="card">
85
- <h2>Crew / Agents</h2>
86
- <div class="stat-grid">
87
- <span class="label">Active Agents</span><span class="value" id="cr-agents">0</span>
88
- <span class="label">Current Turn</span><span class="value" id="cr-turn">0</span>
89
- <span class="label">Status</span><span class="value" id="cr-status">idle</span>
90
- </div>
91
- </div>
92
- <div class="card legend">
93
- <h2>What these numbers mean</h2>
94
- <ul class="legend-list">
95
- <li><b>Tokens saved</b> — conversation space this extension has freed up for you (it compacted old text into short summaries).</li>
96
- <li><b>Tokens stored</b> — how much "memory" (compact summaries) the extension is currently holding for this repo.</li>
97
- <li><b>Injected</b> — times old context was automatically pasted back in because it was relevant to your current task.</li>
98
- <li><b>Recall relevance</b> — of those, how often the recalled context was actually on-topic.</li>
99
- <li><b>Storage dedup</b> — how often new content matched something already saved, so a duplicate copy was skipped (saves space).</li>
100
- <li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
101
- </ul>
102
- <p class="legend-note">Hover any label above for a quick explanation.</p>
103
- </div>
104
- </div>
105
-
106
- <div class="events">
107
- <h2>Event Stream</h2>
108
- <div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
109
- </div>
110
-
111
- <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
112
- <table class="repos">
113
- <thead>
114
- <tr>
115
- <th>Repo</th><th>Model</th>
116
- <th style="text-align:right">Checkpoints</th>
117
- <th style="text-align:right">Tokens Saved</th>
118
- <th style="text-align:right">Retained</th>
119
- <th style="text-align:right">Last Compacted</th>
120
- </tr>
121
- </thead>
122
- <tbody id="cur-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
123
- </table>
124
- <div class="updated" id="cur-updated"></div>
125
-
126
- <div class="updated" id="updated"></div>
127
- </div><!-- /panel-current -->
128
-
129
- `;
130
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Document opening: DOCTYPE, <html>, <head> opening + meta/title.
3
- *
4
- * Styles are injected between this fragment and `bodyOpen()` by the
5
- * `dashboardHtml` composer in `../html-template.ts`.
6
- */
7
- export function headOpen() {
8
- return `<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="utf-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1">
13
- <title>mega-compact dashboard</title>
14
- `;
15
- // trailing \n preserves the original blank-line separation to <style> in the composer
16
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * "High Score" tab — FUTURE / PLACEHOLDER.
3
- *
4
- * Reserved for the next project milestone: a per-repo + global
5
- * compaction-savings leaderboard ("high score" — most tokens saved, best
6
- * recall relevance, longest context-window extension streak, etc.).
7
- *
8
- * Ships as a visible-but-inert stub so the tab nav, routing, and styles are
9
- * in place; the data model + scoring logic land in a follow-up. The composer
10
- * in `../html-template.ts` includes this panel; `script.ts` wires
11
- * `highscore` into the tab-switch `panels` map so clicking the tab reveals
12
- * this placeholder.
13
- */
14
- export function highScoreTab() {
15
- return `<!-- High Score (FUTURE / placeholder for the next project milestone) -->
16
- <div class="tab-panel" id="panel-highscore">
17
- <div class="placeholder">
18
- <div class="em">🏆</div>
19
- <h2>High Score</h2>
20
- <p>A compaction-savings leaderboard is coming in the next project milestone — per-repo and global rankings for tokens saved, recall relevance, and context-window extension streaks. Stay tuned.</p>
21
- </div>
22
- </div>
23
-
24
- `;
25
- }