pi-mega-compact 0.13.1 → 0.13.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/api-contracts/embedder-health.js +11 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +225 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/types.js +1 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +5 -224
- package/dist/extensions/dashboard-server/api-contracts/raptor.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.test/_helpers.js +32 -0
- package/dist/extensions/dashboard-server/routes-cache.test/_helpers.js +48 -0
- package/dist/extensions/dashboard-server/routes-embedder-health.js +67 -0
- package/dist/extensions/dashboard-server/routes-raptor.js +88 -0
- package/dist/extensions/dashboard-server/routes-setup.js +1 -1
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +5 -1
- package/dist/extensions/mega-cache-replay.test/_helpers.js +175 -0
- package/dist/extensions/mega-commands/dataCommands.js +151 -0
- package/dist/extensions/mega-commands/helpers.js +61 -0
- package/dist/extensions/mega-commands/historyCommands.js +97 -0
- package/dist/extensions/mega-commands/setupCommand.js +200 -0
- package/dist/extensions/mega-commands.js +14 -499
- package/dist/extensions/mega-compact-s38.test/_helpers.js +194 -0
- package/dist/extensions/mega-compact.test/_helpers.js +225 -0
- package/dist/extensions/mega-events/agent-handlers/agentEndHandler.js +211 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.js +49 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.js +33 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.js +387 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/event.js +7 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.js +55 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.js +12 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.js +22 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.js +38 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler.js +36 -0
- package/dist/extensions/mega-events/agent-handlers.js +4 -799
- package/dist/extensions/mega-events/context-handler/afterCompact.js +163 -0
- package/dist/extensions/mega-events/context-handler/messageText.js +12 -0
- package/dist/extensions/mega-events/context-handler/tailResult.js +46 -0
- package/dist/extensions/mega-events/context-handler.js +11 -198
- package/dist/extensions/mega-events/separated-prompt.test/_helpers.js +25 -0
- package/dist/src/config.js +2 -0
- package/dist/src/dedup-engine.test/_helpers.js +55 -0
- package/dist/src/e2e.test/_helpers.js +52 -0
- package/dist/src/embedder.js +1 -0
- package/dist/src/httpEmbedder.js +6 -0
- package/dist/src/hyde.js +146 -0
- package/dist/src/importance.test/_helpers.js +5 -0
- package/dist/src/memoryGraph/sources/checkpoints.js +78 -0
- package/dist/src/memoryGraph/sources/edges.js +17 -0
- package/dist/src/memoryGraph/sources/flags.js +22 -0
- package/dist/src/memoryGraph/sources/helpers.js +14 -0
- package/dist/src/memoryGraph/sources/memories.js +88 -0
- package/dist/src/memoryGraph/sources/raptor.js +26 -0
- package/dist/src/memoryGraph/sources/turns.js +176 -0
- package/dist/src/memoryGraph/sources.js +15 -441
- package/dist/src/memoryGraph-gates.test/_helpers.js +120 -0
- package/dist/src/ratio.bench.test/_helpers.js +198 -0
- package/dist/src/recall/async.js +112 -0
- package/dist/src/recall/format.js +149 -0
- package/dist/src/recall/memory.js +62 -0
- package/dist/src/recall/reformulate.js +82 -0
- package/dist/src/recall/sync.js +169 -0
- package/dist/src/recall/types.js +1 -0
- package/dist/src/recall.js +13 -530
- package/dist/src/store/sqlite/schema/core.js +232 -0
- package/dist/src/store/sqlite/schema/game.js +65 -0
- package/dist/src/store/sqlite/schema/plan-v2.js +107 -0
- package/dist/src/store/sqlite/schema/turns.js +59 -0
- package/dist/src/store/sqlite/schema.js +8 -441
- package/dist/src/store/turns/contract-compliance.test/_helpers.js +21 -0
- package/dist/src/store/turns/sqlite-store/admin.js +171 -0
- package/dist/src/store/turns/sqlite-store/ctx.js +1 -0
- package/dist/src/store/turns/sqlite-store/read.js +111 -0
- package/dist/src/store/turns/sqlite-store/rows.js +42 -0
- package/dist/src/store/turns/sqlite-store/write.js +102 -0
- package/dist/src/store/turns/sqlite-store.js +31 -367
- package/dist/src/vectorStore/class.js +356 -0
- package/dist/src/vectorStore/hash.js +10 -0
- package/dist/src/vectorStore/types.js +2 -0
- package/dist/src/vectorStore.js +8 -359
- package/dist/src/vectorStore.test/_helpers.js +17 -0
- package/extensions/dashboard-client/dist/assets/{CacheTab-8HRroK9K.js → CacheTab-DPRHjYZ0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-8HRroK9K.js.map → CacheTab-DPRHjYZ0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-ncobZw-I.js → EventsTab-CgwYq-Qf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-ncobZw-I.js.map → EventsTab-CgwYq-Qf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-C6BI1EZG.js → HealthTab-BgOCjVzR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-C6BI1EZG.js.map → HealthTab-BgOCjVzR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-B88XTCMk.js → MetricsTab-CcH9fP8P.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-B88XTCMk.js.map → MetricsTab-CcH9fP8P.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CGZrSia-.js → OverviewTab-BOs8ZNev.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CGZrSia-.js.map → OverviewTab-BOs8ZNev.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BZo5cNS1.js → ReposTab-B-MfSPvg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BZo5cNS1.js.map → ReposTab-B-MfSPvg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C2RokJcw.js → SessionsTab-BmeN649Q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C2RokJcw.js.map → SessionsTab-BmeN649Q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CewYB2aA.js → TimeSavedCard-DnPg0L0z.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CewYB2aA.js.map → TimeSavedCard-DnPg0L0z.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DpVNIuaL.js → TopicsTab-BODHOg_b.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DpVNIuaL.js.map → TopicsTab-BODHOg_b.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-KeuKKVC9.js → TurnsTab-BK0I7hKr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-KeuKKVC9.js.map → TurnsTab-BK0I7hKr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Cucgohga.js → index-mOlljTFI.js} +9 -9
- package/extensions/dashboard-client/dist/assets/index-mOlljTFI.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-DJsqdPXC.js → useSSE-BYOtv1-3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DJsqdPXC.js.map → useSSE-BYOtv1-3.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/client.ts +16 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/ActionsCard.tsx +178 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DbStatsCard.tsx +140 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DebugBundleCard.tsx +109 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/HealthMitigationCard.tsx +98 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/SchemaHealthCard.tsx +108 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +12 -598
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/MemoryMapView.tsx +476 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +163 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +56 -466
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderHealthCard.tsx +109 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +468 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +13 -483
- package/extensions/dashboard-server/api-contracts/embedder-health.ts +32 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +360 -0
- package/extensions/dashboard-server/api-contracts/endpoints/types.ts +350 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +27 -648
- package/extensions/dashboard-server/api-contracts/index.ts +6 -0
- package/extensions/dashboard-server/api-contracts/raptor.ts +49 -0
- package/extensions/dashboard-server/api-contracts.test/_helpers.ts +47 -0
- package/extensions/dashboard-server/routes-cache.test/_helpers.ts +94 -0
- package/extensions/dashboard-server/routes-embedder-health.ts +77 -0
- package/extensions/dashboard-server/routes-raptor.ts +128 -0
- package/extensions/dashboard-server/routes-setup.ts +1 -1
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +4 -0
- package/extensions/mega-cache-replay.test/_helpers.ts +188 -0
- package/extensions/mega-commands/dataCommands.ts +169 -0
- package/extensions/mega-commands/helpers.ts +62 -0
- package/extensions/mega-commands/historyCommands.ts +117 -0
- package/extensions/mega-commands/setupCommand.ts +235 -0
- package/extensions/mega-commands.ts +25 -538
- package/extensions/mega-compact-s38.test/_helpers.ts +201 -0
- package/extensions/mega-compact.test/_helpers.ts +237 -0
- package/extensions/mega-events/agent-handlers/agentEndHandler.ts +238 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.ts +74 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.ts +50 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.ts +421 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/event.ts +16 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.ts +73 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.ts +29 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.ts +42 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.ts +61 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler.ts +64 -0
- package/extensions/mega-events/agent-handlers.ts +15 -861
- package/extensions/mega-events/context-handler/afterCompact.ts +213 -0
- package/extensions/mega-events/context-handler/messageText.ts +20 -0
- package/extensions/mega-events/context-handler/tailResult.ts +63 -0
- package/extensions/mega-events/context-handler.ts +17 -235
- package/extensions/mega-events/separated-prompt.test/_helpers.ts +35 -0
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dedup-engine.test/_helpers.ts +79 -0
- package/src/e2e.test/_helpers.ts +59 -0
- package/src/embedder.ts +4 -0
- package/src/httpEmbedder.ts +7 -0
- package/src/hyde.ts +162 -0
- package/src/importance.test/_helpers.ts +5 -0
- package/src/memoryGraph/sources/checkpoints.ts +99 -0
- package/src/memoryGraph/sources/edges.ts +23 -0
- package/src/memoryGraph/sources/flags.ts +26 -0
- package/src/memoryGraph/sources/helpers.ts +13 -0
- package/src/memoryGraph/sources/memories.ts +108 -0
- package/src/memoryGraph/sources/raptor.ts +37 -0
- package/src/memoryGraph/sources/turns.ts +226 -0
- package/src/memoryGraph/sources.ts +19 -544
- package/src/memoryGraph-gates.test/_helpers.ts +164 -0
- package/src/ratio.bench.test/_helpers.ts +268 -0
- package/src/recall/async.ts +142 -0
- package/src/recall/format.ts +173 -0
- package/src/recall/memory.ts +90 -0
- package/src/recall/reformulate.ts +111 -0
- package/src/recall/sync.ts +196 -0
- package/src/recall/types.ts +63 -0
- package/src/recall.ts +25 -712
- package/src/store/sqlite/schema/core.ts +233 -0
- package/src/store/sqlite/schema/game.ts +66 -0
- package/src/store/sqlite/schema/plan-v2.ts +108 -0
- package/src/store/sqlite/schema/turns.ts +60 -0
- package/src/store/sqlite/schema.ts +13 -441
- package/src/store/turns/contract-compliance.test/_helpers.ts +38 -0
- package/src/store/turns/sqlite-store/admin.ts +249 -0
- package/src/store/turns/sqlite-store/ctx.ts +14 -0
- package/src/store/turns/sqlite-store/read.ts +174 -0
- package/src/store/turns/sqlite-store/rows.ts +55 -0
- package/src/store/turns/sqlite-store/write.ts +193 -0
- package/src/store/turns/sqlite-store.ts +33 -514
- package/src/vectorStore/class.ts +433 -0
- package/src/vectorStore/hash.ts +11 -0
- package/src/vectorStore/types.ts +58 -0
- package/src/vectorStore.test/_helpers.ts +20 -0
- package/src/vectorStore.ts +13 -489
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DZi1MnZI.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DZi1MnZI.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CjUa9zzw.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CjUa9zzw.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-C_BwmebC.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-C_BwmebC.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-Cucgohga.js.map +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/cacheStripe.ts — P3.5 topic-shift stripe refresh.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from turnEndHandler.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions limit. When MEGACOMPACT_CACHE_STRIPING is ON, embed
|
|
6
|
+
* the current turn, detect topic shift vs. the previous turn, and refresh cache
|
|
7
|
+
* stripe assignments when shifted (or first turn). Best-effort: failures log +
|
|
8
|
+
* continue, never break the agent loop.
|
|
9
|
+
*/
|
|
10
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
11
|
+
import type { MegaConfig } from "../../../mega-config.js";
|
|
12
|
+
import { defaultEmbedder } from "../../../../src/embedder.js";
|
|
13
|
+
import { storeTopicEmbedding, loadTopicEmbedding, detectTopicShift } from "../../separated-prompt.js";
|
|
14
|
+
import { refreshStripeAssignments as writeStripeAssignments } from "../../../../src/cache-stripe-impl.js";
|
|
15
|
+
import type { TurnEndEvent } from "./event.js";
|
|
16
|
+
|
|
17
|
+
/** P3.5: topic-shift stripe refresh. Best-effort + non-fatal. */
|
|
18
|
+
export function cacheStripe(
|
|
19
|
+
event: TurnEndEvent,
|
|
20
|
+
runtime: MegaRuntime,
|
|
21
|
+
config: MegaConfig,
|
|
22
|
+
): void {
|
|
23
|
+
if (config.cacheStriping) {
|
|
24
|
+
try {
|
|
25
|
+
const stateDir = runtime.currentStateDir;
|
|
26
|
+
const sessionId = runtime.rt.sessionId;
|
|
27
|
+
const currentTurn = event.turnIndex;
|
|
28
|
+
const embedder = defaultEmbedder();
|
|
29
|
+
|
|
30
|
+
// (a) Extract text from the assistant's response for topic embedding.
|
|
31
|
+
let textToEmbed = "";
|
|
32
|
+
const msg = event.message;
|
|
33
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
34
|
+
for (const part of msg.content) {
|
|
35
|
+
if ("text" in part && typeof part.text === "string") {
|
|
36
|
+
textToEmbed += part.text + " ";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
textToEmbed = textToEmbed.trim() || `turn-${currentTurn}`;
|
|
41
|
+
const embedding = new Float32Array(embedder.embed(textToEmbed));
|
|
42
|
+
storeTopicEmbedding(stateDir, sessionId, currentTurn, embedding);
|
|
43
|
+
|
|
44
|
+
// (b) Load previous turn's embedding and detect topic shift.
|
|
45
|
+
const prevEmb = currentTurn > 0
|
|
46
|
+
? loadTopicEmbedding(stateDir, sessionId, currentTurn - 1)
|
|
47
|
+
: null;
|
|
48
|
+
const shifted = currentTurn === 0 || detectTopicShift(embedding, prevEmb);
|
|
49
|
+
|
|
50
|
+
// (c) If shifted (or first turn), refresh stripe assignments (WRITE path).
|
|
51
|
+
let chunkCount = 0;
|
|
52
|
+
if (shifted) {
|
|
53
|
+
chunkCount = writeStripeAssignments(
|
|
54
|
+
stateDir,
|
|
55
|
+
undefined,
|
|
56
|
+
embedder,
|
|
57
|
+
(detail) => runtime.logger.info("stripe_refresh_detail", {
|
|
58
|
+
detail,
|
|
59
|
+
turnIndex: currentTurn,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// (d) Log the refresh outcome.
|
|
65
|
+
runtime.logger.info("stripe_refresh", {
|
|
66
|
+
turnIndex: currentTurn,
|
|
67
|
+
shifted,
|
|
68
|
+
chunkCount,
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
/* non-fatal: stripe refresh never breaks the agent loop */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/contextHealth.ts — v0.12 context-health mitigation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from turnEndHandler.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions limit. Computes + persists the health score and
|
|
6
|
+
* acts on mitigation signals (force-compact / prefix-break) here at turn_end,
|
|
7
|
+
* where ctx is in scope. Non-fatal.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
11
|
+
import { piCompactWouldNoop } from "../../../mega-pipeline.js";
|
|
12
|
+
import type { MegaConfig } from "../../../mega-config.js";
|
|
13
|
+
import { handleTurnEndHealth } from "../../health-handler.js";
|
|
14
|
+
import type { TurnEndEvent } from "./event.js";
|
|
15
|
+
|
|
16
|
+
/** v0.12: context-health mitigation. Non-fatal end-to-end. */
|
|
17
|
+
export function contextHealth(
|
|
18
|
+
event: TurnEndEvent,
|
|
19
|
+
ctx: ExtensionContext,
|
|
20
|
+
runtime: MegaRuntime,
|
|
21
|
+
config: MegaConfig,
|
|
22
|
+
): void {
|
|
23
|
+
// v0.12: Context Health — compute + persist health score. Non-fatal.
|
|
24
|
+
// Mitigation signals are acted on here (ctx is in scope at turn_end).
|
|
25
|
+
const healthSignal = handleTurnEndHealth(event, runtime, config);
|
|
26
|
+
if (healthSignal.forceCompact) {
|
|
27
|
+
runtime.logger?.info("health_mitigate_compact", {
|
|
28
|
+
composite: healthSignal.composite,
|
|
29
|
+
turnIndex: event.turnIndex,
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
// Force a compaction to flush degraded context. Same race-guarded
|
|
33
|
+
// deferred path as the critical-over escape hatch above.
|
|
34
|
+
if (!piCompactWouldNoop(ctx)) {
|
|
35
|
+
ctx.compact();
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
/* non-fatal: mitigation never breaks the agent loop */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (healthSignal.breakPrefix) {
|
|
42
|
+
runtime.logger?.info("health_mitigate_prefix_break", {
|
|
43
|
+
turnIndex: event.turnIndex,
|
|
44
|
+
});
|
|
45
|
+
// Invalidate the cached trim so the next context event regenerates
|
|
46
|
+
// from scratch — the prefix change forces a cache miss, bypassing
|
|
47
|
+
// the corrupted KV state.
|
|
48
|
+
runtime.trimCache = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/errorRetry.ts — S38/R1-R11 error-retry safety net.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from turnEndHandler.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions limit. Catches ALL error types (provider failure,
|
|
6
|
+
* network timeout, 5xx, 429, auth, compaction-noop, poisoned-context) that
|
|
7
|
+
* surface at turn_end. Non-fatal: wrapped in try/catch so a classifier/retry
|
|
8
|
+
* failure never breaks the agent loop. PREVENT-PI-003: retry nudge fires via
|
|
9
|
+
* pi.sendUserMessage (user-role). R1-R4 (retry redesign) bound the loop:
|
|
10
|
+
* in-flight dedup, gating backoff, session-global cap, poisoned-context
|
|
11
|
+
* detection.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
15
|
+
import { piCompactWouldNoop } from "../../../mega-pipeline.js";
|
|
16
|
+
import type { MegaConfig } from "../../../mega-config.js";
|
|
17
|
+
import {
|
|
18
|
+
classifyError,
|
|
19
|
+
classifyErrorDetailed,
|
|
20
|
+
errorRetryBackoffMs,
|
|
21
|
+
extractErrorSignature,
|
|
22
|
+
isKnownRetryableTransient,
|
|
23
|
+
} from "../../error-classifier.js";
|
|
24
|
+
import { safeSendUserMessage } from "../../send-safe.js";
|
|
25
|
+
import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
|
|
26
|
+
import type { TurnEndEvent } from "./event.js";
|
|
27
|
+
|
|
28
|
+
/** S38: broader error-retry safety net. Non-fatal end-to-end. */
|
|
29
|
+
export async function errorRetry(
|
|
30
|
+
event: TurnEndEvent,
|
|
31
|
+
ctx: ExtensionContext,
|
|
32
|
+
pi: ExtensionAPI,
|
|
33
|
+
runtime: MegaRuntime,
|
|
34
|
+
config: MegaConfig,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
// S38: broader error-retry safety net. S28 only catches stopReason==='length';
|
|
37
|
+
// this catches ALL other error types (provider failure, network timeout, 5xx,
|
|
38
|
+
// 429, auth, compaction-noop, poisoned-context) that surface at turn_end.
|
|
39
|
+
// Non-fatal: wrapped in try/catch so a classifier/retry failure never breaks
|
|
40
|
+
// the agent loop. PREVENT-PI-003: retry nudge fires via pi.sendUserMessage
|
|
41
|
+
// (user-role). R1-R4 (retry redesign) bound the loop: in-flight dedup,
|
|
42
|
+
// gating backoff, session-global cap, poisoned-context detection.
|
|
43
|
+
try {
|
|
44
|
+
// (1) S28 owns length — skip the classifier entirely for it.
|
|
45
|
+
const sr = (event.message as { stopReason?: string } | undefined)
|
|
46
|
+
?.stopReason;
|
|
47
|
+
if (sr === "length") {
|
|
48
|
+
// S28 handles; nothing for S38 to do here.
|
|
49
|
+
} else {
|
|
50
|
+
const category = classifyError(event.message);
|
|
51
|
+
const detail = classifyErrorDetailed(event.message);
|
|
52
|
+
runtime.lastErrorCategory = category;
|
|
53
|
+
// R3: stateful poisoned-context signal — repeated identical error text
|
|
54
|
+
// across consecutive turns. The classifier is stateless, so this
|
|
55
|
+
// upgrade happens here. A 'transient' (or 'poisoned-context') turn
|
|
56
|
+
// whose normalized error signature matches the previous one bumps
|
|
57
|
+
// errorTextRepeatCount; once it crosses the threshold, the category
|
|
58
|
+
// is upgraded to 'poisoned-context' (the request is deterministic) —
|
|
59
|
+
// R7: unless the signature carries a known-retryable network/
|
|
60
|
+
// throughput marker (guard below). Conservative: only upgrade, never
|
|
61
|
+
// downgrade, and only for transient (a 'permanent' auth error
|
|
62
|
+
// repeating is still permanent).
|
|
63
|
+
let effectiveCategory = category;
|
|
64
|
+
const errSig = extractErrorSignature(event.message);
|
|
65
|
+
if (category === "transient" || category === "poisoned-context") {
|
|
66
|
+
if (errSig) {
|
|
67
|
+
if (runtime.rt.lastErrorText === errSig) {
|
|
68
|
+
runtime.rt.errorTextRepeatCount++;
|
|
69
|
+
} else {
|
|
70
|
+
runtime.rt.lastErrorText = errSig;
|
|
71
|
+
runtime.rt.errorTextRepeatCount = 1;
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
runtime.rt.errorTextRepeatCount >=
|
|
75
|
+
config.poisonedContextRepeatThreshold &&
|
|
76
|
+
category === "transient"
|
|
77
|
+
) {
|
|
78
|
+
// R3/R7: upgrade only when the repeating error carries NO
|
|
79
|
+
// known-retryable marker. Network/throughput failures (timeout,
|
|
80
|
+
// ECONNRESET, 5xx, 429, connection lost, …) that repeat are
|
|
81
|
+
// still retryable — the API is just flaky, and /clear cannot fix
|
|
82
|
+
// them (2026-07-30 false-alarm incident). Only upgrade
|
|
83
|
+
// transient → poisoned-context for errors that repeat identically
|
|
84
|
+
// with no known-retryable cause, like the 2026-07-28 incident
|
|
85
|
+
// where the provider returned "Request failed — please retry."
|
|
86
|
+
// deterministically. The marker set is shared with classifyError
|
|
87
|
+
// (error-classifier.ts) so the two never drift.
|
|
88
|
+
if (!isKnownRetryableTransient(errSig)) {
|
|
89
|
+
effectiveCategory = "poisoned-context";
|
|
90
|
+
} else {
|
|
91
|
+
runtime.logger.info("repeat-upgrade-declined", {
|
|
92
|
+
sessionId: runtime.rt.sessionId,
|
|
93
|
+
signature: (errSig || '').slice(0, 500),
|
|
94
|
+
repeatCount: runtime.rt.errorTextRepeatCount,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Non-transient (success, cancelled, compaction-noop, context-overflow,
|
|
101
|
+
// permanent): a fresh non-identical signal clears the repeat tracker.
|
|
102
|
+
runtime.rt.lastErrorText = undefined;
|
|
103
|
+
runtime.rt.errorTextRepeatCount = 0;
|
|
104
|
+
}
|
|
105
|
+
if (effectiveCategory === null) {
|
|
106
|
+
// (3) success / normal flow / unknown-but-non-retryable — reset.
|
|
107
|
+
runtime.rt.errorRetryCount = 0;
|
|
108
|
+
runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset on success
|
|
109
|
+
// R4: a successful assistant turn consumes any queued nudge.
|
|
110
|
+
runtime.rt.retryNudgePending = false;
|
|
111
|
+
// R10: reset outage advisory so a recovered-then-flapping
|
|
112
|
+
// provider re-advises once per episode.
|
|
113
|
+
runtime.rt.providerOutageAdvised = false;
|
|
114
|
+
} else if (effectiveCategory === "compaction-noop") {
|
|
115
|
+
// (4) pi race / manual compact catch — NOT retryable. The compaction
|
|
116
|
+
// already succeeded via pi's native path; retrying would race again
|
|
117
|
+
// (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
|
|
118
|
+
// the original error WITHOUT firing a retry nudge.
|
|
119
|
+
runtime.rt.errorRetryCount = 0;
|
|
120
|
+
runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
|
|
121
|
+
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
122
|
+
runtime.dashboard.event("compaction_noop_diagnostic", {
|
|
123
|
+
turnIndex: event.turnIndex,
|
|
124
|
+
sessionId: runtime.rt.sessionId,
|
|
125
|
+
});
|
|
126
|
+
runtime.logger.info("compaction-noop-diagnostic", {
|
|
127
|
+
sessionId: runtime.rt.sessionId,
|
|
128
|
+
turnIndex: event.turnIndex,
|
|
129
|
+
});
|
|
130
|
+
} else if (effectiveCategory === "cancelled") {
|
|
131
|
+
// (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
|
|
132
|
+
// nudging would restart a task the user explicitly stopped.
|
|
133
|
+
// Reset both counters (a cancel is not an error for circuit-breaker
|
|
134
|
+
// purposes). Emit a diagnostic so the dashboard shows it.
|
|
135
|
+
runtime.rt.errorRetryCount = 0;
|
|
136
|
+
runtime.rt.consecutiveErrors = 0;
|
|
137
|
+
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
138
|
+
runtime.dashboard.event("error_retry_cancelled", {
|
|
139
|
+
turnIndex: event.turnIndex,
|
|
140
|
+
sessionId: runtime.rt.sessionId,
|
|
141
|
+
});
|
|
142
|
+
runtime.logger.info("error-retry-cancelled", {
|
|
143
|
+
sessionId: runtime.rt.sessionId,
|
|
144
|
+
turnIndex: event.turnIndex,
|
|
145
|
+
});
|
|
146
|
+
} else if (effectiveCategory === "context-overflow") {
|
|
147
|
+
// (4b) context-window overflow 400 ("too long... even after compaction").
|
|
148
|
+
// NOT a blind retry: re-submitting the same oversized prompt would just
|
|
149
|
+
// re-400 and busy-loop. Reset the counters (this turn is terminal, not
|
|
150
|
+
// retryable in the S38 sense) and force ONE best-effort re-compact with
|
|
151
|
+
// the debounce bypassed + the same race-guard (lastNativeCompactAt
|
|
152
|
+
// cooldown + deferred setTimeout re-check) as the agent_end durable
|
|
153
|
+
// trim. Resume after that forced compact is handled by the existing
|
|
154
|
+
// nudgeResume() inside session_before_compact (it fires after
|
|
155
|
+
// driveNativeCompaction supplies the compaction) — do NOT add a
|
|
156
|
+
// separate nudge here.
|
|
157
|
+
runtime.rt.errorRetryCount = 0;
|
|
158
|
+
runtime.rt.consecutiveErrors = 0;
|
|
159
|
+
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
160
|
+
runtime.dashboard.event("context_overflow", {
|
|
161
|
+
turnIndex: event.turnIndex,
|
|
162
|
+
sessionId: runtime.rt.sessionId,
|
|
163
|
+
});
|
|
164
|
+
runtime.logger.warn("context-overflow", {
|
|
165
|
+
sessionId: runtime.rt.sessionId,
|
|
166
|
+
turnIndex: event.turnIndex,
|
|
167
|
+
});
|
|
168
|
+
if (config.auto) {
|
|
169
|
+
const now2 = Date.now();
|
|
170
|
+
const cooldownMs2 = config.raceGuardStrict ? 30_000 : 10_000;
|
|
171
|
+
const sinceCompact2 = now2 - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
172
|
+
if (sinceCompact2 >= cooldownMs2 && !piCompactWouldNoop(ctx)) {
|
|
173
|
+
runtime.debounceUntil = now2 + 0;
|
|
174
|
+
const stamp2 = runtime.rt.lastNativeCompactAt;
|
|
175
|
+
const liveSid2 = runtime.rt.sessionId;
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
try {
|
|
178
|
+
if (runtime.rt.sessionId !== liveSid2) return; // session reset
|
|
179
|
+
const since3 =
|
|
180
|
+
Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
181
|
+
if (
|
|
182
|
+
runtime.rt.lastNativeCompactAt !== stamp2 &&
|
|
183
|
+
since3 < cooldownMs2
|
|
184
|
+
)
|
|
185
|
+
return;
|
|
186
|
+
if (piCompactWouldNoop(ctx)) return;
|
|
187
|
+
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. Forced re-compact after a context-overflow 400.
|
|
188
|
+
} catch {
|
|
189
|
+
/* non-fatal */
|
|
190
|
+
}
|
|
191
|
+
}, 500);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else if (effectiveCategory === "poisoned-context") {
|
|
195
|
+
// R3: poisoned context — a DETERMINISTIC request-rejection that
|
|
196
|
+
// retrying cannot fix (0-token 'error', generic "request failed",
|
|
197
|
+
// 400 non-overflow, or repeated identical error text). NO blind
|
|
198
|
+
// retry nudge — re-submitting the same poisoned prompt re-triggers
|
|
199
|
+
// the same rejection (the 2026-07-28 incident: ~60-message spam).
|
|
200
|
+
// Instead: (a) dashboard event + warn log naming the suspected
|
|
201
|
+
// poison; (b) ONE user-role advise message (/clear or /new) per
|
|
202
|
+
// session, throttled; (c) if config.auto, ONE guarded compact per
|
|
203
|
+
// error signature (same race-guarded deferred path as context-
|
|
204
|
+
// overflow) — only if not already attempted for this signature.
|
|
205
|
+
runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
|
|
206
|
+
runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
|
|
207
|
+
runtime.rt.poisonedCount++; // R7: dashboard counter
|
|
208
|
+
const sig = errSig || "unknown";
|
|
209
|
+
// (a) dashboard + log — R11: include signal + rawText for forensics
|
|
210
|
+
runtime.dashboard.event("poisoned_context", {
|
|
211
|
+
signature: sig,
|
|
212
|
+
repeatCount: runtime.rt.errorTextRepeatCount,
|
|
213
|
+
turnIndex: event.turnIndex,
|
|
214
|
+
sessionId: runtime.rt.sessionId,
|
|
215
|
+
signal: detail.signal,
|
|
216
|
+
rawText: (errSig || '').slice(0, 500),
|
|
217
|
+
...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
|
|
218
|
+
});
|
|
219
|
+
runtime.logger.warn("poisoned-context", {
|
|
220
|
+
sessionId: runtime.rt.sessionId,
|
|
221
|
+
turnIndex: event.turnIndex,
|
|
222
|
+
signature: sig,
|
|
223
|
+
repeatCount: runtime.rt.errorTextRepeatCount,
|
|
224
|
+
signal: detail.signal,
|
|
225
|
+
rawText: (errSig || '').slice(0, 500),
|
|
226
|
+
...(detail.httpStatus !== undefined ? { httpStatus: detail.httpStatus } : {}),
|
|
227
|
+
});
|
|
228
|
+
// (b) throttle flag: set unconditionally so the advise fires at
|
|
229
|
+
// most once per session regardless of channel.
|
|
230
|
+
runtime.rt.poisonedAdviseSent = true;
|
|
231
|
+
// Legacy flag-OFF path: user-role advise message.
|
|
232
|
+
// Default ON (advisoryChannel=true): dashboard-only, no user message.
|
|
233
|
+
if (!config.advisoryChannel) {
|
|
234
|
+
// PREVENT-PI-003: user-role sendUserMessage only.
|
|
235
|
+
await safeSendUserMessage(
|
|
236
|
+
pi,
|
|
237
|
+
"[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
// (c) one guarded compact per error signature (attempt to remove
|
|
241
|
+
// the poisoned region). Race-guarded + deferred, mirroring the
|
|
242
|
+
// context-overflow path. Only if not already attempted for this
|
|
243
|
+
// signature (poisonedCompactSignatures).
|
|
244
|
+
if (config.auto && !runtime.rt.poisonedCompactSignatures.has(sig)) {
|
|
245
|
+
runtime.rt.poisonedCompactSignatures.add(sig);
|
|
246
|
+
const nowP = Date.now();
|
|
247
|
+
const cooldownMsP = config.raceGuardStrict ? 30_000 : 10_000;
|
|
248
|
+
const sinceCompactP = nowP - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
249
|
+
if (sinceCompactP >= cooldownMsP && !piCompactWouldNoop(ctx)) {
|
|
250
|
+
runtime.debounceUntil = nowP + 0;
|
|
251
|
+
const stampP = runtime.rt.lastNativeCompactAt;
|
|
252
|
+
const liveSidP = runtime.rt.sessionId;
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
try {
|
|
255
|
+
if (runtime.rt.sessionId !== liveSidP) return; // session reset
|
|
256
|
+
const sinceP =
|
|
257
|
+
Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
258
|
+
if (
|
|
259
|
+
runtime.rt.lastNativeCompactAt !== stampP &&
|
|
260
|
+
sinceP < cooldownMsP
|
|
261
|
+
)
|
|
262
|
+
return;
|
|
263
|
+
if (piCompactWouldNoop(ctx)) return;
|
|
264
|
+
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. One guarded compact after a poisoned-context detection.
|
|
265
|
+
} catch {
|
|
266
|
+
/* non-fatal */
|
|
267
|
+
}
|
|
268
|
+
}, 500);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// (5) transient or permanent — retry with exponential backoff.
|
|
273
|
+
// S38.7: hard-stop switch — bypass ALL retry logic when set.
|
|
274
|
+
if (config.errorRetryHardStop) {
|
|
275
|
+
runtime.rt.errorRetryCount = 0;
|
|
276
|
+
runtime.dashboard.event("error_retry_disabled", {
|
|
277
|
+
category: effectiveCategory,
|
|
278
|
+
turnIndex: event.turnIndex,
|
|
279
|
+
reason: "hard-stop",
|
|
280
|
+
});
|
|
281
|
+
return; // early exit — no retry
|
|
282
|
+
}
|
|
283
|
+
// S38.6: circuit-breaker — stop retrying after too many consecutive errors.
|
|
284
|
+
runtime.rt.consecutiveErrors++;
|
|
285
|
+
// R10: send calm "provider outage" advisory once per episode.
|
|
286
|
+
await maybeSendProviderOutageAdvisory(
|
|
287
|
+
effectiveCategory, runtime, pi, config,
|
|
288
|
+
{ signal: detail.signal, rawText: (errSig || '').slice(0, 500) },
|
|
289
|
+
);
|
|
290
|
+
if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
|
|
291
|
+
runtime.dashboard.event("error_retry_circuit_open", {
|
|
292
|
+
consecutive: runtime.rt.consecutiveErrors,
|
|
293
|
+
max: config.maxConsecutiveErrors,
|
|
294
|
+
turnIndex: event.turnIndex,
|
|
295
|
+
});
|
|
296
|
+
runtime.logger.warn("error-retry-circuit-open", {
|
|
297
|
+
sessionId: runtime.rt.sessionId,
|
|
298
|
+
consecutive: runtime.rt.consecutiveErrors,
|
|
299
|
+
max: config.maxConsecutiveErrors,
|
|
300
|
+
});
|
|
301
|
+
return; // early exit — circuit breaker tripped
|
|
302
|
+
}
|
|
303
|
+
const max =
|
|
304
|
+
effectiveCategory === "transient"
|
|
305
|
+
? config.autoRetryTransientMax
|
|
306
|
+
: config.autoRetryPermanentMax;
|
|
307
|
+
// max === 0 disables the category entirely (revert to S28-only).
|
|
308
|
+
if (max <= 0) {
|
|
309
|
+
runtime.rt.errorRetryCount = 0;
|
|
310
|
+
} else {
|
|
311
|
+
runtime.rt.errorRetryCount++;
|
|
312
|
+
if (runtime.rt.errorRetryCount > max) {
|
|
313
|
+
// Exhausted — surface the error, reset for the next burst.
|
|
314
|
+
runtime.dashboard.event("error_retry_exhausted", {
|
|
315
|
+
category: effectiveCategory,
|
|
316
|
+
count: runtime.rt.errorRetryCount,
|
|
317
|
+
max,
|
|
318
|
+
turnIndex: event.turnIndex,
|
|
319
|
+
});
|
|
320
|
+
runtime.logger.info("error-retry-exhausted", {
|
|
321
|
+
sessionId: runtime.rt.sessionId,
|
|
322
|
+
category: effectiveCategory,
|
|
323
|
+
count: runtime.rt.errorRetryCount,
|
|
324
|
+
max,
|
|
325
|
+
});
|
|
326
|
+
runtime.rt.errorRetryCount = 0;
|
|
327
|
+
} else {
|
|
328
|
+
// R2: session-global cap — total S38 nudges per session across
|
|
329
|
+
// ALL bursts. Independent of the per-burst max and the circuit
|
|
330
|
+
// breaker. Hitting it is terminal for the session: log +
|
|
331
|
+
// dashboard event, stop nudging. `0` disables (reverts to
|
|
332
|
+
// per-burst + circuit-breaker only).
|
|
333
|
+
if (
|
|
334
|
+
config.errorRetrySessionMax > 0 &&
|
|
335
|
+
runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
|
|
336
|
+
) {
|
|
337
|
+
runtime.dashboard.event("error_retry_session_exhausted", {
|
|
338
|
+
count: runtime.rt.errorRetrySessionCount,
|
|
339
|
+
max: config.errorRetrySessionMax,
|
|
340
|
+
category: effectiveCategory,
|
|
341
|
+
turnIndex: event.turnIndex,
|
|
342
|
+
});
|
|
343
|
+
runtime.logger.warn("error-retry-session-exhausted", {
|
|
344
|
+
sessionId: runtime.rt.sessionId,
|
|
345
|
+
count: runtime.rt.errorRetrySessionCount,
|
|
346
|
+
max: config.errorRetrySessionMax,
|
|
347
|
+
category: effectiveCategory,
|
|
348
|
+
});
|
|
349
|
+
runtime.rt.errorRetryCount = 0;
|
|
350
|
+
return; // terminal for the session — no nudge
|
|
351
|
+
}
|
|
352
|
+
// R1: in-flight nudge dedup — a nudge queued via
|
|
353
|
+
// deliverAs:'followUp' must not be re-sent until it has been
|
|
354
|
+
// consumed by an actual new agent turn (turn_start resets
|
|
355
|
+
// retryNudgePending). Without this, a fast-erroring provider +
|
|
356
|
+
// a per-turn nudge → N nudges queue up and pi dispatches N
|
|
357
|
+
// retry turns, each re-submitting the same failing prompt
|
|
358
|
+
// (the 2026-07-28 incident). errorRetryCount still advances
|
|
359
|
+
// (this IS an error turn), so the per-burst max + circuit
|
|
360
|
+
// breaker still bound the burst.
|
|
361
|
+
if (runtime.rt.retryNudgePending) {
|
|
362
|
+
runtime.dashboard.event("error_retry_dedup_skip", {
|
|
363
|
+
category: effectiveCategory,
|
|
364
|
+
count: runtime.rt.errorRetryCount,
|
|
365
|
+
max,
|
|
366
|
+
turnIndex: event.turnIndex,
|
|
367
|
+
});
|
|
368
|
+
return; // pending nudge not yet consumed — skip
|
|
369
|
+
}
|
|
370
|
+
// R1: gating backoff — errorRetryUntil is now GATING
|
|
371
|
+
// (previously documented as non-gating). A nudge cannot fire
|
|
372
|
+
// before the previous backoff elapses. This paces retries
|
|
373
|
+
// (5s/10s/20s/30s by default) so a fast-erroring provider
|
|
374
|
+
// doesn't slam N nudges in <1s.
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
if (now < runtime.rt.errorRetryUntil) {
|
|
377
|
+
runtime.dashboard.event("error_retry_backoff_skip", {
|
|
378
|
+
category: effectiveCategory,
|
|
379
|
+
count: runtime.rt.errorRetryCount,
|
|
380
|
+
max,
|
|
381
|
+
turnIndex: event.turnIndex,
|
|
382
|
+
});
|
|
383
|
+
return; // backoff not elapsed — skip
|
|
384
|
+
}
|
|
385
|
+
// Fire the retry nudge. Set pending (R1 dedup) + backoff
|
|
386
|
+
// (R1 pacing) + session count (R2 cap) BEFORE the await so a
|
|
387
|
+
// re-entrant turn_end during the send can't double-fire.
|
|
388
|
+
runtime.rt.retryNudgePending = true;
|
|
389
|
+
runtime.rt.lastErrorRetryAt = now;
|
|
390
|
+
runtime.rt.errorRetryUntil =
|
|
391
|
+
now +
|
|
392
|
+
errorRetryBackoffMs(
|
|
393
|
+
runtime.rt.errorRetryCount,
|
|
394
|
+
config.errorRetryBackoffMs,
|
|
395
|
+
);
|
|
396
|
+
runtime.rt.errorRetrySessionCount++;
|
|
397
|
+
runtime.dashboard.event("error_retry", {
|
|
398
|
+
category: effectiveCategory,
|
|
399
|
+
count: runtime.rt.errorRetryCount,
|
|
400
|
+
max,
|
|
401
|
+
turnIndex: event.turnIndex,
|
|
402
|
+
});
|
|
403
|
+
runtime.logger.info("error-retry", {
|
|
404
|
+
sessionId: runtime.rt.sessionId,
|
|
405
|
+
category: effectiveCategory,
|
|
406
|
+
count: runtime.rt.errorRetryCount,
|
|
407
|
+
max,
|
|
408
|
+
});
|
|
409
|
+
// PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
|
|
410
|
+
await safeSendUserMessage(
|
|
411
|
+
pi,
|
|
412
|
+
"[mega-compact] the last turn ended with an error; please retry.",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
/* non-fatal: a classifier/retry failure never breaks the agent loop */
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/event.ts — structural shape of the pi `turn_end` event.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the turnEndHandler helpers so each stays agnostic of the full
|
|
5
|
+
* event shape.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Structural shape of the pi `turn_end` event used by this handler. */
|
|
9
|
+
export interface TurnEndEvent {
|
|
10
|
+
turnIndex: number;
|
|
11
|
+
message: {
|
|
12
|
+
role?: string;
|
|
13
|
+
content?: unknown;
|
|
14
|
+
stopReason?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/gameScoring.ts — S33/S35 game-mode scoring + achievements.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from turnEndHandler.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions limit. Records turns + cache metrics per repo, and
|
|
6
|
+
* arms the MEGA CACHE flare (oopsie gag) when the real dedup hit rate exceeds
|
|
7
|
+
* 100%. Gated behind game_mode_on (no scoring when off). Best-effort +
|
|
8
|
+
* non-fatal: a scoring failure must never break the agent loop (G6).
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
12
|
+
import { resolveRepoRoot } from "../../../mega-config.js";
|
|
13
|
+
import { recordScore, readLatestCacheHitPct } from "../../../../src/store/sqlite.js";
|
|
14
|
+
import { evaluateAndUnlockAchievements } from "../../../../src/store/sqlite/game-achievements.js";
|
|
15
|
+
import { isMegaCache } from "../../../../src/game/scoring.js";
|
|
16
|
+
import { vectorStats } from "../../../../src/vectorStore.js";
|
|
17
|
+
import type { TurnEndEvent } from "./event.js";
|
|
18
|
+
|
|
19
|
+
/** S33+S35: game-mode scoring + achievement evaluation. Best-effort. */
|
|
20
|
+
export function gameScoring(
|
|
21
|
+
event: TurnEndEvent,
|
|
22
|
+
ctx: ExtensionContext,
|
|
23
|
+
runtime: MegaRuntime,
|
|
24
|
+
): void {
|
|
25
|
+
// S33: game-mode scoring — record turns + cache metrics per repo, and arm
|
|
26
|
+
// the MEGA CACHE flare (oopsie gag) when the real dedup hit rate exceeds
|
|
27
|
+
// 100%. Gated behind game_mode_on (no scoring when off). Best-effort +
|
|
28
|
+
// non-fatal: a scoring failure must never break the agent loop (G6).
|
|
29
|
+
try {
|
|
30
|
+
if (runtime.getCachedGameState().game_mode_on) {
|
|
31
|
+
const repo = resolveRepoRoot(ctx.cwd) ?? runtime.currentStateDir;
|
|
32
|
+
const st = vectorStats(runtime.store, runtime.rt.sessionId);
|
|
33
|
+
// C.3: prefer provider cache hit rate, fall back to dedup hit rate
|
|
34
|
+
const providerPct = readLatestCacheHitPct(runtime.currentStateDir);
|
|
35
|
+
const cachePct =
|
|
36
|
+
providerPct != null ? providerPct : st.dedupHitRate * 100;
|
|
37
|
+
const modelId = runtime.currentModel?.modelId ?? "unknown";
|
|
38
|
+
recordScore(runtime.currentStateDir, {
|
|
39
|
+
repo_root: repo,
|
|
40
|
+
metric: "turns",
|
|
41
|
+
value: runtime.currentTurn,
|
|
42
|
+
meta: { modelId, turnIndex: event.turnIndex },
|
|
43
|
+
});
|
|
44
|
+
recordScore(runtime.currentStateDir, {
|
|
45
|
+
repo_root: repo,
|
|
46
|
+
metric: "cache",
|
|
47
|
+
value: cachePct,
|
|
48
|
+
meta: {
|
|
49
|
+
hits: st.dedupCollapsed + runtime.rt.recallInjections,
|
|
50
|
+
lookups: st.checkpointCount,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
// MEGA CACHE: the real ratio >1 (dedupHitRate>1) → trophy row + flare.
|
|
54
|
+
if (isMegaCache(cachePct)) {
|
|
55
|
+
recordScore(runtime.currentStateDir, {
|
|
56
|
+
repo_root: repo,
|
|
57
|
+
metric: "mega_cache",
|
|
58
|
+
value: cachePct,
|
|
59
|
+
meta: { peakPct: cachePct, firstSeenTs: Date.now() },
|
|
60
|
+
});
|
|
61
|
+
runtime.armMegaCacheFlare(cachePct);
|
|
62
|
+
}
|
|
63
|
+
// S35: evaluate achievements after scoring; arm a one-time flare for
|
|
64
|
+
// the newly-unlocked ones (consumed by snapshot() → widget toast).
|
|
65
|
+
const newTitles = evaluateAndUnlockAchievements(
|
|
66
|
+
runtime.currentStateDir,
|
|
67
|
+
);
|
|
68
|
+
if (newTitles.length) runtime.armAchievementFlare(newTitles);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* non-fatal: scoring must never break the agent loop */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/lengthStop.ts — S28 max-output-token truncation detection.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from turnEndHandler.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions limit. Detects when event.message.stopReason is
|
|
6
|
+
* 'length' (generation hit max_tokens OUTPUT cap) and arms the agent_end nudge.
|
|
7
|
+
*/
|
|
8
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
9
|
+
import type { MegaConfig } from "../../../mega-config.js";
|
|
10
|
+
import type { TurnEndEvent } from "./event.js";
|
|
11
|
+
|
|
12
|
+
/** S28: detect max-output-token truncation and arm the agent_end nudge. */
|
|
13
|
+
export function lengthStop(
|
|
14
|
+
event: TurnEndEvent,
|
|
15
|
+
runtime: MegaRuntime,
|
|
16
|
+
config: MegaConfig,
|
|
17
|
+
): void {
|
|
18
|
+
// S28: detect max-output-token truncation. event.message.stopReason is the
|
|
19
|
+
// pi-ai StopReason union; 'length' == generation hit max_tokens OUTPUT cap
|
|
20
|
+
// (INPUT-orthogonal to context-window overflow). Arm the agent_end nudge.
|
|
21
|
+
if (
|
|
22
|
+
config.autoContinueLengthStop &&
|
|
23
|
+
event.message.role === "assistant" &&
|
|
24
|
+
event.message.stopReason === "length"
|
|
25
|
+
) {
|
|
26
|
+
runtime.rt.lengthStopPending = true;
|
|
27
|
+
runtime.dashboard.event("length_stop", { turnIndex: event.turnIndex });
|
|
28
|
+
}
|
|
29
|
+
}
|