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.
- package/README.md +6 -0
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +51 -0
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +63 -0
- package/dist/extensions/mega-compact-s38.test.js +28 -2
- package/dist/extensions/mega-events/agent-handlers.js +16 -0
- package/dist/extensions/mega-events/error-classifier.js +8 -3
- package/extensions/dashboard-client/package-lock.json +1 -1
- package/extensions/dashboard-client/package.json +1 -1
- package/extensions/mega-compact-s38.test.ts +32 -2
- package/extensions/mega-events/agent-handlers.ts +15 -0
- package/extensions/mega-events/error-classifier.ts +8 -2
- package/package.json +1 -1
- package/dist/extensions/dashboard-server/helpers.js +0 -37
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +0 -26
- package/dist/extensions/dashboard-server/html/body-open.js +0 -23
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +0 -130
- package/dist/extensions/dashboard-server/html/head-open.js +0 -16
- package/dist/extensions/dashboard-server/html/high-score-tab.js +0 -25
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +0 -26
- package/dist/extensions/dashboard-server/html/script.js +0 -259
- package/dist/extensions/dashboard-server/html/styles.js +0 -103
- package/dist/extensions/dashboard-server/html/summary-tab.js +0 -19
- package/dist/extensions/dashboard-server/html-template.js +0 -41
- package/dist/src/store/sqlite/connection.js +0 -35
- package/dist/src/store/sqlite/index-store.js +0 -167
- package/dist/src/store/sqlite/memory.js +0 -54
- package/dist/src/store/sqlite/minhash-lsh.js +0 -47
- package/dist/src/store/sqlite/sessions.js +0 -39
- package/dist/src/store/sqlite/transaction.js +0 -19
- package/dist/src/vectorStore/add.js +0 -260
- package/dist/src/vectorStore/dedup.js +0 -52
- package/dist/src/vectorStore/index.js +0 -10
- package/dist/src/vectorStore/queries.js +0 -83
- package/dist/src/vectorStore/search.js +0 -95
- package/dist/src/vectorStore/session.js +0 -19
- package/dist/src/vectorStore/store.js +0 -105
- package/dist/src/vectorStore/types.js +0 -6
- 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
|
+
[](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
|
|
125
|
+
test("S38: classifyError returns 'transient' for error stopReason", () => {
|
|
126
126
|
assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
|
|
127
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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))
|
|
@@ -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
|
|
124
|
+
test("S38: classifyError returns 'transient' for error stopReason", () => {
|
|
125
125
|
assert.equal(classifyErrorFn({ stopReason: "error" }), "transient");
|
|
126
|
-
|
|
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
|
-
|
|
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.
|
|
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 & 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
|
-
}
|