pi-mega-compact 0.11.3 → 0.11.5
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/endpoints.js +19 -0
- package/dist/extensions/dashboard-server/api-contracts/memory.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +1 -0
- package/dist/extensions/dashboard-server/api-contracts/setup.js +10 -0
- package/dist/extensions/dashboard-server/index-reader.js +6 -2
- package/dist/extensions/dashboard-server/routes-cache.js +115 -55
- package/dist/extensions/dashboard-server/routes-memory.js +51 -0
- package/dist/extensions/dashboard-server/routes-repo.js +2 -2
- package/dist/extensions/dashboard-server/routes-setup.js +162 -0
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +7 -1
- package/dist/extensions/mega-commands.js +189 -1
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +41 -6
- package/dist/extensions/mega-events/compact-handlers.js +1 -0
- package/dist/extensions/mega-events/context-handler.js +37 -92
- package/dist/extensions/mega-events/context-hwm.js +51 -0
- package/dist/extensions/mega-events/error-classifier.js +118 -128
- package/dist/extensions/mega-events/mirror-append.js +107 -0
- package/dist/extensions/mega-events/outage-advisor.js +59 -0
- package/dist/extensions/mega-events/perf-handler.js +51 -1
- package/dist/extensions/mega-events/recall-tail.js +59 -0
- package/dist/extensions/mega-events/session-handlers.js +15 -8
- package/dist/extensions/mega-runtime/reset-runtime.js +6 -0
- package/dist/extensions/mega-runtime/runtime.js +7 -0
- package/dist/src/embedder.js +41 -2
- package/dist/src/memoryStats.js +75 -0
- package/dist/src/prefix-break.js +43 -0
- package/dist/src/pricing.js +1 -1
- package/dist/src/store/sqlite/perf-samples.js +144 -16
- package/dist/src/store/turns/memory-store.js +8 -0
- package/dist/src/store/turns/sqlite-store.js +14 -3
- package/dist/src/store/turns/types.js +18 -1
- package/dist/src/store/vectorIndex.js +26 -5
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js → AchievementTiles-BoL7qBOJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-vEo2MtE2.js.map → AchievementTiles-BoL7qBOJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-BFDE3HYc.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DjxfYrDc.js.map → AchievementsTab-BFDE3HYc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js → ConfigTab-DDFE-ckA.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-Cm6ymbmx.js.map → ConfigTab-DDFE-ckA.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js → EventsTab-B6UxZ7PM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DXWl_YzI.js.map → EventsTab-B6UxZ7PM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-BLDudcO5.js → GameTab-DehUX-KE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-BLDudcO5.js.map → GameTab-DehUX-KE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js → MaintenanceTab-0GWRt4wn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DLkKCztn.js.map → MaintenanceTab-0GWRt4wn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js → MetricsTab-C3rwMVsN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Cq4u-rZF.js.map → MetricsTab-C3rwMVsN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-39O9_x5j.js +2 -0
- package/extensions/dashboard-client/dist/assets/{OverviewTab-D4kGvsiQ.js.map → OverviewTab-39O9_x5j.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-Cfj6Q5sH.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-Cfj6Q5sH.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js → SessionsTab-DXDTCi3r.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DZC2-GCn.js.map → SessionsTab-DXDTCi3r.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-Cw1dJ4e9.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-Cw1dJ4e9.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js → TopicsTab-BAw972BV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-01G71hv1.js.map → TopicsTab-BAw972BV.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js → TurnsTab-Cf5mGIdl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DLRg0LZT.js.map → TurnsTab-Cf5mGIdl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js +2 -0
- package/extensions/dashboard-client/dist/assets/format-COjDIVGj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-BX0bcgLu.js → index-4gQ7auad.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js +2 -0
- package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js → useSSE-DNgMuyP6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BBYt5UPB.js.map → useSSE-DNgMuyP6.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/App.tsx +4 -0
- package/extensions/dashboard-client/src/api/client.ts +14 -0
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +21 -12
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +56 -0
- package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +38 -57
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +1 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +355 -0
- package/extensions/dashboard-client/tsconfig.json +3 -2
- package/extensions/dashboard-client/vite.config.ts +2 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +36 -3
- package/extensions/dashboard-server/api-contracts/index.ts +7 -0
- package/extensions/dashboard-server/api-contracts/memory.ts +27 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +231 -231
- package/extensions/dashboard-server/api-contracts/provider-cache.ts +37 -0
- package/extensions/dashboard-server/api-contracts/setup.ts +58 -0
- package/extensions/dashboard-server/index-reader.ts +138 -121
- package/extensions/dashboard-server/routes-cache.ts +161 -85
- package/extensions/dashboard-server/routes-memory.ts +63 -0
- package/extensions/dashboard-server/routes-repo.ts +36 -36
- package/extensions/dashboard-server/routes-setup.ts +181 -0
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +6 -0
- package/extensions/mega-commands.ts +211 -1
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-events/agent-handlers.ts +44 -5
- package/extensions/mega-events/compact-handlers.ts +1 -0
- package/extensions/mega-events/context-handler.ts +45 -110
- package/extensions/mega-events/context-hwm.ts +85 -0
- package/extensions/mega-events/error-classifier.ts +156 -124
- package/extensions/mega-events/mirror-append.ts +155 -0
- package/extensions/mega-events/outage-advisor.ts +79 -0
- package/extensions/mega-events/perf-handler.ts +79 -5
- package/extensions/mega-events/recall-tail.ts +88 -0
- package/extensions/mega-events/session-handlers.ts +14 -7
- package/extensions/mega-runtime/helpers.ts +14 -0
- package/extensions/mega-runtime/reset-runtime.ts +6 -0
- package/extensions/mega-runtime/runtime.ts +7 -0
- package/package.json +5 -2
- package/src/embedder.ts +42 -2
- package/src/memoryStats.ts +107 -0
- package/src/prefix-break.ts +83 -0
- package/src/pricing.ts +2 -2
- package/src/store/sqlite/perf-samples.ts +215 -22
- package/src/store/turns/memory-store.ts +10 -1
- package/src/store/turns/sqlite-store.ts +27 -15
- package/src/store/turns/types.ts +21 -0
- package/src/store/vectorIndex.ts +33 -4
- package/dist/extensions/conflict-scan.test.js +0 -115
- package/dist/extensions/dashboard-server/api-contracts.test.js +0 -884
- package/dist/extensions/dashboard-server/html.test.js +0 -52
- package/dist/extensions/dashboard-server/perf-server.test.js +0 -80
- package/dist/extensions/dashboard-server/routes-cache.test.js +0 -248
- package/dist/extensions/dashboard-server/server.test.js +0 -120
- package/dist/extensions/dashboard-server-s32.test.js +0 -181
- package/dist/extensions/dashboard-server.test.js +0 -339
- package/dist/extensions/mega-cache-replay.test.js +0 -400
- package/dist/extensions/mega-compact-s38.test.js +0 -820
- package/dist/extensions/mega-compact.test.js +0 -1112
- package/dist/extensions/mega-events.test.js +0 -47
- package/dist/extensions/mega-game-cmds.test.js +0 -163
- package/dist/extensions/mega-runtime/snapshot.test.js +0 -127
- package/dist/extensions/mega-runtime/state.test.js +0 -266
- package/dist/extensions/mega-runtime/widget.test.js +0 -343
- package/dist/extensions/mega-shutdown-widget.test.js +0 -121
- package/dist/extensions/mega-teamrun.test.js +0 -156
- package/dist/extensions/mega-trim.test.js +0 -58
- package/dist/extensions/mega-turn-store.test.js +0 -68
- package/dist/src/boundary.test.js +0 -170
- package/dist/src/compact.test.js +0 -78
- package/dist/src/config/themes.test.js +0 -94
- package/dist/src/dedup/dedup.test.js +0 -41
- package/dist/src/dedup/l1.test.js +0 -50
- package/dist/src/dedup/raptor/buildHistory.test.js +0 -292
- package/dist/src/dedup/raptor/multilevel-serve.test.js +0 -229
- package/dist/src/dedup/raptor/multilevel.test.js +0 -238
- package/dist/src/dedup/raptor/promote.test.js +0 -69
- package/dist/src/dedup/raptor/raptor.test.js +0 -248
- package/dist/src/dedup/raptor/retrieval.test.js +0 -95
- package/dist/src/dedup/raptor/serve-gate.test.js +0 -298
- package/dist/src/dedup/sprint12.test.js +0 -219
- package/dist/src/dedup-engine.test.js +0 -472
- package/dist/src/driftDetection.test.js +0 -87
- package/dist/src/e2e.test.js +0 -698
- package/dist/src/engine.test.js +0 -111
- package/dist/src/extractive.test.js +0 -147
- package/dist/src/fork.test.js +0 -81
- package/dist/src/game/scoring.test.js +0 -98
- package/dist/src/httpEmbedder.test.js +0 -277
- package/dist/src/importance.test.js +0 -372
- package/dist/src/intent.test.js +0 -78
- package/dist/src/log.test.js +0 -42
- package/dist/src/mechanical-fix.test.js +0 -65
- package/dist/src/memory.test.js +0 -70
- package/dist/src/memoryConsolidate.test.js +0 -38
- package/dist/src/memoryOps.test.js +0 -129
- package/dist/src/memoryRecall.test.js +0 -171
- package/dist/src/memoryRoundtrip.test.js +0 -137
- package/dist/src/metrics/turns.test.js +0 -152
- package/dist/src/mirror/mirror.test.js +0 -185
- package/dist/src/raptor-inject-summaries.test.js +0 -162
- package/dist/src/ratio.bench.test.js +0 -897
- package/dist/src/recall.integration.test.js +0 -77
- package/dist/src/recall.test.js +0 -379
- package/dist/src/sprint14.test.js +0 -219
- package/dist/src/sprint4x-rag-verification.test.js +0 -93
- package/dist/src/store/compression.test.js +0 -139
- package/dist/src/store/memoryIndex.test.js +0 -51
- package/dist/src/store/migrate.test.js +0 -139
- package/dist/src/store/pgOpenGuard.test.js +0 -74
- package/dist/src/store/phase01.test.js +0 -81
- package/dist/src/store/phase04.test.js +0 -54
- package/dist/src/store/sprint10.test.js +0 -186
- package/dist/src/store/sqlite/game-achievements.test.js +0 -67
- package/dist/src/store/sqlite/game-scores.test.js +0 -106
- package/dist/src/store/sqlite/game-state.test.js +0 -76
- package/dist/src/store/sqlite/global-index.test.js +0 -136
- package/dist/src/store/sqlite/mechanical-fix.test.js +0 -146
- package/dist/src/store/sqlite/perf-samples.test.js +0 -146
- package/dist/src/store/sqlite/raptor.test.js +0 -106
- package/dist/src/store/sqlite/turns.test.js +0 -183
- package/dist/src/store/sqlite.cachehit.test.js +0 -55
- package/dist/src/store/sqlite.dbmaint.test.js +0 -158
- package/dist/src/store/sqlite.dbmirror.test.js +0 -184
- package/dist/src/store/turns/contract-compliance.test.js +0 -428
- package/dist/src/store/turns/memory-store.test.js +0 -9
- package/dist/src/store/turns/migrations.test.js +0 -138
- package/dist/src/store/turns/sqlite-store.test.js +0 -144
- package/dist/src/store/vectorIndex.test.js +0 -123
- package/dist/src/store.test.js +0 -161
- package/dist/src/supersede.test.js +0 -36
- package/dist/src/topics/cluster.test.js +0 -150
- package/dist/src/topics/store.test.js +0 -117
- package/dist/src/types.test.js +0 -9
- package/dist/src/vector-search-cache.test.js +0 -157
- package/dist/src/vectorStore.test.js +0 -479
- package/dist/src/wiki.test.js +0 -171
- package/extensions/conflict-scan.test.ts +0 -129
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-D4kGvsiQ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-BX0bcgLu.js.map +0 -1
- package/extensions/dashboard-server/api-contracts.test.ts +0 -1065
- package/extensions/dashboard-server/html.test.ts +0 -62
- package/extensions/dashboard-server/perf-server.test.ts +0 -101
- package/extensions/dashboard-server/routes-cache.test.ts +0 -302
- package/extensions/dashboard-server/server.test.ts +0 -131
- package/extensions/dashboard-server-s32.test.ts +0 -195
- package/extensions/dashboard-server.test.ts +0 -382
- package/extensions/mega-cache-replay.test.ts +0 -526
- package/extensions/mega-compact-s38.test.ts +0 -872
- package/extensions/mega-compact.test.ts +0 -1509
- package/extensions/mega-events.test.ts +0 -55
- package/extensions/mega-game-cmds.test.ts +0 -197
- package/extensions/mega-runtime/snapshot.test.ts +0 -139
- package/extensions/mega-runtime/state.test.ts +0 -262
- package/extensions/mega-runtime/widget.test.ts +0 -468
- package/extensions/mega-shutdown-widget.test.ts +0 -141
- package/extensions/mega-teamrun.test.ts +0 -176
- package/extensions/mega-trim.test.ts +0 -64
- package/extensions/mega-turn-store.test.ts +0 -108
- package/src/boundary.test.ts +0 -187
- package/src/compact.test.ts +0 -99
- package/src/config/themes.test.ts +0 -116
- package/src/dedup/dedup.test.ts +0 -46
- package/src/dedup/l1.test.ts +0 -57
- package/src/dedup/raptor/buildHistory.test.ts +0 -353
- package/src/dedup/raptor/multilevel-serve.test.ts +0 -273
- package/src/dedup/raptor/multilevel.test.ts +0 -325
- package/src/dedup/raptor/promote.test.ts +0 -82
- package/src/dedup/raptor/raptor.test.ts +0 -297
- package/src/dedup/raptor/retrieval.test.ts +0 -118
- package/src/dedup/raptor/serve-gate.test.ts +0 -348
- package/src/dedup/sprint12.test.ts +0 -242
- package/src/dedup-engine.test.ts +0 -670
- package/src/driftDetection.test.ts +0 -100
- package/src/e2e.test.ts +0 -843
- package/src/engine.test.ts +0 -123
- package/src/extractive.test.ts +0 -174
- package/src/fork.test.ts +0 -94
- package/src/game/scoring.test.ts +0 -103
- package/src/httpEmbedder.test.ts +0 -286
- package/src/importance.test.ts +0 -538
- package/src/intent.test.ts +0 -87
- package/src/log.test.ts +0 -47
- package/src/mechanical-fix.test.ts +0 -70
- package/src/memory.test.ts +0 -92
- package/src/memoryConsolidate.test.ts +0 -47
- package/src/memoryOps.test.ts +0 -148
- package/src/memoryRecall.test.ts +0 -194
- package/src/memoryRoundtrip.test.ts +0 -155
- package/src/metrics/turns.test.ts +0 -165
- package/src/mirror/mirror.test.ts +0 -240
- package/src/raptor-inject-summaries.test.ts +0 -228
- package/src/ratio.bench.test.ts +0 -1316
- package/src/recall.integration.test.ts +0 -96
- package/src/recall.test.ts +0 -467
- package/src/sprint14.test.ts +0 -245
- package/src/sprint4x-rag-verification.test.ts +0 -119
- package/src/store/compression.test.ts +0 -168
- package/src/store/memoryIndex.test.ts +0 -61
- package/src/store/migrate.test.ts +0 -158
- package/src/store/pgOpenGuard.test.ts +0 -89
- package/src/store/phase01.test.ts +0 -88
- package/src/store/phase04.test.ts +0 -58
- package/src/store/sprint10.test.ts +0 -214
- package/src/store/sqlite/game-achievements.test.ts +0 -80
- package/src/store/sqlite/game-scores.test.ts +0 -132
- package/src/store/sqlite/game-state.test.ts +0 -89
- package/src/store/sqlite/global-index.test.ts +0 -222
- package/src/store/sqlite/mechanical-fix.test.ts +0 -162
- package/src/store/sqlite/perf-samples.test.ts +0 -169
- package/src/store/sqlite/raptor.test.ts +0 -139
- package/src/store/sqlite/turns.test.ts +0 -218
- package/src/store/sqlite.cachehit.test.ts +0 -68
- package/src/store/sqlite.dbmaint.test.ts +0 -193
- package/src/store/sqlite.dbmirror.test.ts +0 -229
- package/src/store/turns/contract-compliance.test.ts +0 -562
- package/src/store/turns/memory-store.test.ts +0 -15
- package/src/store/turns/migrations.test.ts +0 -161
- package/src/store/turns/sqlite-store.test.ts +0 -161
- package/src/store/vectorIndex.test.ts +0 -140
- package/src/store.test.ts +0 -191
- package/src/supersede.test.ts +0 -42
- package/src/topics/cluster.test.ts +0 -176
- package/src/topics/store.test.ts +0 -132
- package/src/types.test.ts +0 -10
- package/src/vector-search-cache.test.ts +0 -190
- package/src/vectorStore.test.ts +0 -533
- package/src/wiki.test.ts +0 -217
|
@@ -23,49 +23,48 @@
|
|
|
23
23
|
* - `etimedout` is Node's timeout errno lowercased — does NOT contain the
|
|
24
24
|
* substring "timeout".
|
|
25
25
|
* - `socket hang up` is Node's ECONNRESET *message*; the errno lives in
|
|
26
|
-
* error.code, which extractErrorSignature never sees.
|
|
27
|
-
|
|
26
|
+
* error.code, which extractErrorSignature never sees.
|
|
27
|
+
* - router phrasings (all targets failed / no healthy target / too many
|
|
28
|
+
* concurrent) -- pi's status prefix is console-only (2026-07-30 incident). */
|
|
29
|
+
export const KNOWN_RETRYABLE_TRANSIENT_PATTERN = /max(imum)? output token|rate[\s.-]?limit|429|too many requests|too many concurrent|overloaded|5\d\d|internal server|bad gateway|service unavailable|all targets failed|no healthy target|fetch failed|network|time(?:d[\s-]?out|out)|etimedout|econnreset|econnrefused|epipe|eai_again|socket|closed unexpectedly|premature close|other side closed|connection (lost|refused|reset|aborted?|was closed)|stream (interrupted|closed|ended|failed)|disconnected/;
|
|
28
30
|
/** R7: true when the error text carries a known-retryable transient marker.
|
|
29
31
|
* Defensively lowercases so callers can pass raw or normalized text. */
|
|
30
32
|
export function isKnownRetryableTransient(text) {
|
|
31
33
|
return KNOWN_RETRYABLE_TRANSIENT_PATTERN.test(text.toLowerCase());
|
|
32
34
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
/** R8: best-effort extraction of HTTP status from pi AgentMessage shapes. */
|
|
36
|
+
function extractHttpStatus(m) {
|
|
37
|
+
for (const outer of [m.error, m]) {
|
|
38
|
+
if (!outer || typeof outer !== 'object')
|
|
39
|
+
continue;
|
|
40
|
+
for (const key of ['status', 'statusCode', 'code']) {
|
|
41
|
+
const v = outer[key];
|
|
42
|
+
if (typeof v === 'number' && v >= 100 && v <= 599)
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** R11: classify a turn-end error/stop signal into a retry category with a
|
|
49
|
+
* diagnostic signal tag naming the rule that fired, plus the raw httpStatus
|
|
50
|
+
* when extractHttpStatus found one. */
|
|
51
|
+
export function classifyErrorDetailed(message) {
|
|
43
52
|
let text = '';
|
|
44
|
-
// R3: usage-token signal. Only when `usage` is explicitly present with
|
|
45
|
-
// inputTokens=0 AND outputTokens=0 do we know the turn never reached the
|
|
46
|
-
// model. An ABSENT usage field means "unknown" (the event didn't carry
|
|
47
|
-
// token counts) — we do NOT treat that as the 0-token poisoned signal,
|
|
48
|
-
// because mid-response stream deaths and partial-content turns routinely
|
|
49
|
-
// omit usage while still being transient (preserves all pre-R3 tests).
|
|
50
53
|
let usagePresent = false;
|
|
51
54
|
let totalTokens = 0;
|
|
55
|
+
let httpStatus;
|
|
52
56
|
if (typeof message === 'string') {
|
|
53
57
|
text = message;
|
|
54
58
|
}
|
|
55
59
|
else if (message && typeof message === 'object') {
|
|
56
60
|
const m = message;
|
|
57
61
|
const sr = typeof m.stopReason === 'string' ? m.stopReason : '';
|
|
58
|
-
// S28 guard: length stopReason is handled exclusively by the S28 path.
|
|
59
62
|
if (sr === 'length')
|
|
60
|
-
return null;
|
|
61
|
-
// User ESC / Ctrl-C abort — stopReason === 'aborted'. Not retryable:
|
|
62
|
-
// nudging would restart a task the user explicitly stopped.
|
|
63
|
+
return { category: null, signal: 'length-guard' };
|
|
63
64
|
if (sr === 'aborted')
|
|
64
|
-
return 'cancelled';
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return null;
|
|
68
|
-
// R3: extract usage tokens for the 0-token poisoned-context signal.
|
|
65
|
+
return { category: 'cancelled', signal: 'cancelled' };
|
|
66
|
+
if (/^(stop|tool_?use|end_?turn|max_?tokens|complete|finished|done)$/i.test(sr))
|
|
67
|
+
return { category: null, signal: 'success' };
|
|
69
68
|
const usage = m.usage;
|
|
70
69
|
usagePresent = usage != null && typeof usage === 'object';
|
|
71
70
|
if (usagePresent) {
|
|
@@ -74,6 +73,7 @@ export function classifyError(message) {
|
|
|
74
73
|
(typeof u.inputTokens === 'number' ? u.inputTokens : 0) +
|
|
75
74
|
(typeof u.outputTokens === 'number' ? u.outputTokens : 0);
|
|
76
75
|
}
|
|
76
|
+
httpStatus = extractHttpStatus(m);
|
|
77
77
|
const parts = [];
|
|
78
78
|
if (sr)
|
|
79
79
|
parts.push(sr);
|
|
@@ -88,128 +88,86 @@ export function classifyError(message) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
if (m.error) {
|
|
91
|
-
// Extract message from error objects for pattern matching.
|
|
92
91
|
const err = m.error;
|
|
93
|
-
if (typeof err === 'string')
|
|
92
|
+
if (typeof err === 'string')
|
|
94
93
|
parts.push(err);
|
|
95
|
-
}
|
|
96
94
|
else if (err && typeof err === 'object') {
|
|
97
95
|
const errObj = err;
|
|
98
|
-
if (typeof errObj.message === 'string')
|
|
96
|
+
if (typeof errObj.message === 'string')
|
|
99
97
|
parts.push(errObj.message);
|
|
100
|
-
|
|
101
|
-
else {
|
|
98
|
+
else
|
|
102
99
|
parts.push(JSON.stringify(err));
|
|
103
|
-
}
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
|
-
// S38: detect mid-response errors where the stream died without a
|
|
107
|
-
// proper stopReason (empty/undefined) — this catches provider failures
|
|
108
|
-
// that cut off the response mid-stream, INCLUDING the case where the
|
|
109
|
-
// provider emitted partial content before dying (a truncated response
|
|
110
|
-
// with no stop reason is a mid-stream death, not a success).
|
|
111
|
-
// A genuine success ALWAYS carries stop/tool_use/toolUse (which
|
|
112
|
-
// short-circuit to null at the top), so any message reaching here with a
|
|
113
|
-
// falsy stopReason is a stream failure → retryable.
|
|
114
102
|
if (!sr) {
|
|
115
|
-
|
|
103
|
+
// A missing stopReason alone is not sufficient evidence of stream death.
|
|
104
|
+
// Require at least one corroborating signal: an error field, or 0 total tokens
|
|
105
|
+
// (indicating the stream died before producing anything). A normal completion
|
|
106
|
+
// from a runtime that omits stopReason has content and tokens — not an error.
|
|
107
|
+
const hasError = (m.error != null && m.error !== undefined && m.error !== '') ||
|
|
108
|
+
(typeof m.errorMessage === 'string' && m.errorMessage !== '');
|
|
109
|
+
if (hasError || (usagePresent && totalTokens === 0)) {
|
|
110
|
+
const r = { category: 'transient', signal: 'stream-death-no-stopreason' };
|
|
111
|
+
if (httpStatus !== undefined)
|
|
112
|
+
r.httpStatus = httpStatus;
|
|
113
|
+
return r;
|
|
114
|
+
}
|
|
115
|
+
// No stopReason, no error field, tokens present — likely a normal completion
|
|
116
|
+
// from a runtime that omits stopReason (pi-crew, etc.). Not an error.
|
|
117
|
+
return { category: null, signal: 'success' };
|
|
116
118
|
}
|
|
117
119
|
text = parts.join(' ');
|
|
118
120
|
}
|
|
119
121
|
if (!text)
|
|
120
|
-
return null;
|
|
122
|
+
return { category: null, signal: 'unknown' };
|
|
121
123
|
const s = text.toLowerCase();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (/already compacted/.test(s))
|
|
126
|
-
return 'compaction-noop';
|
|
127
|
-
if (/compaction failed/.test(s))
|
|
128
|
-
return 'compaction-noop';
|
|
129
|
-
if (/nothing to compact/.test(s))
|
|
130
|
-
return 'compaction-noop';
|
|
131
|
-
if (/auto[\s-]?compaction failed/.test(s))
|
|
132
|
-
return 'compaction-noop';
|
|
133
|
-
// --- context-overflow (ORDER BEFORE generic transient!) ---
|
|
134
|
-
// A 400 from the model meaning "the prompt is bigger than the context window."
|
|
135
|
-
// Catches BOTH provider phrasings so the classification does not depend on
|
|
136
|
-
// which backend the router landed on:
|
|
137
|
-
// - pi/Anthropic wrapper: "too long for this model's context window even
|
|
138
|
-
// after compaction. Reduce the conversation length..." ->
|
|
139
|
-
// too long | context window | even after compaction | reduce the conversation
|
|
140
|
-
// - OpenAI/OpenRouter provider-side (often wrapped in "All targets failed:
|
|
141
|
-
// <model>. Last error: ..."): "This model's maximum context length is N
|
|
142
|
-
// tokens. Your request requires at least M tokens. Please reduce your
|
|
143
|
-
// input or max_tokens." -> maximum context length | context length
|
|
144
|
-
// exceeded | requires at least N tokens | reduce your input
|
|
145
|
-
// All of these carry `invalid_request_error` (UNDERSCORE, not 'invalid
|
|
146
|
-
// request' space) and would otherwise fall through to the generic
|
|
147
|
-
// `s.includes('error')` transient branch below, misclassifying as
|
|
148
|
-
// 'transient' and firing up to 5 blind retry nudges that re-submit the same
|
|
149
|
-
// oversized prompt -> re-400 -> busy-loop. 'context-overflow' instead forces
|
|
150
|
-
// ONE deferred re-compact (debounce-bypassed, race-guarded) and fires NO
|
|
151
|
-
// blind retry nudge. The forced re-compact is shaped by the existing
|
|
152
|
-
// session_before_compact durable trim (it cannot lower pi's firstKeptEntryId).
|
|
124
|
+
if (/already compacted|compaction failed|nothing to compact|auto[\s-]?compaction failed/.test(s)) {
|
|
125
|
+
return { category: 'compaction-noop', signal: 'compaction-noop' };
|
|
126
|
+
}
|
|
153
127
|
if (/too long|context window|maximum context length|context length exceeded|requires at least \d+ tokens|even after compaction|reduce the conversation|reduce your input/.test(s)) {
|
|
154
|
-
return 'context-overflow';
|
|
128
|
+
return { category: 'context-overflow', signal: 'context-overflow' };
|
|
129
|
+
}
|
|
130
|
+
if (isKnownRetryableTransient(s)) {
|
|
131
|
+
const r = { category: 'transient', signal: 'transient-marker' };
|
|
132
|
+
if (httpStatus !== undefined)
|
|
133
|
+
r.httpStatus = httpStatus;
|
|
134
|
+
return r;
|
|
135
|
+
}
|
|
136
|
+
if (httpStatus === 429 || (httpStatus !== undefined && httpStatus >= 500)) {
|
|
137
|
+
return { category: 'transient', signal: 'transient-status', httpStatus };
|
|
138
|
+
}
|
|
139
|
+
if (httpStatus === 401 || httpStatus === 403) {
|
|
140
|
+
return { category: 'permanent', signal: 'permanent-status', httpStatus };
|
|
155
141
|
}
|
|
156
|
-
// --- transient (known-retryable markers FIRST — these override poisoned signals) ---
|
|
157
|
-
// R3: network/throughput failures (timeout, ECONNRESET, 5xx, 429) MUST stay
|
|
158
|
-
// transient even when usage is 0 tokens, because they are retryable and
|
|
159
|
-
// /clear cannot fix them. 'connection aborted' is a network failure
|
|
160
|
-
// (ECONNABORTED), NOT a user ESC (stopReason 'aborted', early-return above).
|
|
161
|
-
// The marker set is shared with the agent-handlers R3 repeat-upgrade guard
|
|
162
|
-
// (isKnownRetryableTransient) so the two never drift.
|
|
163
|
-
if (isKnownRetryableTransient(s))
|
|
164
|
-
return 'transient';
|
|
165
|
-
// --- poisoned-context (R3: ORDER AFTER specific transient markers, BEFORE
|
|
166
|
-
// the generic 'error' transient fallthrough) ---
|
|
167
|
-
// A DETERMINISTIC request-rejection that retrying cannot fix. Re-submitting
|
|
168
|
-
// the same prompt re-triggers the same rejection. The 2026-07-28 incident
|
|
169
|
-
// was a provider returning 0-token "Request failed — please retry." turns
|
|
170
|
-
// for every request; classifyError mapped stopReason 'error' → 'transient'
|
|
171
|
-
// and the session emitted ~9 nudge bursts (~60 context-bloating messages)
|
|
172
|
-
// before stopping.
|
|
173
|
-
//
|
|
174
|
-
// Signal 1: provider request-validation 400 that is NOT context-overflow
|
|
175
|
-
// (context-overflow already returned above). Orphaned tool result / malformed
|
|
176
|
-
// message structure / unexpected role ordering / empty content. These were
|
|
177
|
-
// previously classified 'permanent' (max 1 retry) — but a single retry
|
|
178
|
-
// re-submits the SAME malformed structure and re-400s, so they are poisoned,
|
|
179
|
-
// not permanent. Auth/permission 400s stay permanent (separate check below).
|
|
180
142
|
if (/invalid_request_error|invalid request|malformed|bad request|orphaned tool|tool (result )?(without|for )|unexpected role|role (ordering|sequence)|empty (content|message|request)/.test(s)) {
|
|
181
|
-
return 'poisoned-context';
|
|
143
|
+
return { category: 'poisoned-context', signal: 'poisoned-invalid-request' };
|
|
182
144
|
}
|
|
183
|
-
// Signal 2: generic catch-all "request failed" / "request error" with no
|
|
184
|
-
// specific transient marker (transient markers already returned above).
|
|
185
|
-
// This is the exact phrasing the 2026-07-28 incident used. Without a
|
|
186
|
-
// specific retryable cause, treat the rejection as deterministic.
|
|
187
145
|
if (/request failed|request error/.test(s)) {
|
|
188
|
-
return 'poisoned-context';
|
|
146
|
+
return { category: 'poisoned-context', signal: 'poisoned-request-failed' };
|
|
189
147
|
}
|
|
190
|
-
// Signal 3: stopReason 'error' + usage explicitly 0 tokens (the turn never
|
|
191
|
-
// reached the model). Conservative: only when `usage` is PRESENT and 0 — an
|
|
192
|
-
// absent usage field means "unknown" and stays transient (mid-response
|
|
193
|
-
// deaths / partial-content turns often omit usage). The transient markers
|
|
194
|
-
// above already returned, so reaching here with 'error' text and 0 tokens
|
|
195
|
-
// means the request was rejected before any model work.
|
|
196
148
|
if (usagePresent && totalTokens === 0 && /error/.test(s)) {
|
|
197
|
-
return '
|
|
149
|
+
return { category: 'transient', signal: 'bare-0-token' };
|
|
198
150
|
}
|
|
199
|
-
// --- transient (generic 'error' stopReason fallthrough) ---
|
|
200
|
-
// Reaches here for a generic stopReason 'error' with no specific marker and
|
|
201
|
-
// no 0-token signal (usage absent or > 0). Stays transient — conservative.
|
|
202
151
|
if (s.includes('error') && !/\b(auth|unauthorized|invalid (api )?key|permission)\b/.test(s)) {
|
|
203
|
-
return 'transient'
|
|
152
|
+
return { category: 'transient', signal: 'generic-error' };
|
|
153
|
+
}
|
|
154
|
+
if (/auth|unauthorized|invalid (api )?key|permission/.test(s)) {
|
|
155
|
+
return { category: 'permanent', signal: 'permanent-auth' };
|
|
204
156
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
157
|
+
return { category: null, signal: 'unknown' };
|
|
158
|
+
}
|
|
159
|
+
/** S38.2: classify a turn-end error/stop signal into a retry category.
|
|
160
|
+
*
|
|
161
|
+
* `length` is returned as null — S28 owns the max-output-token length stopReason
|
|
162
|
+
* exclusively (its agent_end nudge path is separate and must not be doubled).
|
|
163
|
+
*
|
|
164
|
+
* Thin wrapper around classifyErrorDetailed — returns only the category.
|
|
165
|
+
*
|
|
166
|
+
* @param message the event.message (a pi AgentMessage) or an error string
|
|
167
|
+
* @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | 'cancelled' | 'poisoned-context' | null (success/unknown)
|
|
168
|
+
*/
|
|
169
|
+
export function classifyError(message) {
|
|
170
|
+
return classifyErrorDetailed(message).category;
|
|
213
171
|
}
|
|
214
172
|
/** S38.2: exponential backoff for error-retry nudges.
|
|
215
173
|
* count is 1-based (the retry about to fire). baseMs is the unit
|
|
@@ -230,10 +188,33 @@ export function errorRetryBackoffMs(count, baseMs = 5000) {
|
|
|
230
188
|
* 'error' for retryable turns) so two turns with the same error content share
|
|
231
189
|
* a signature. Empty string when there is no error text (e.g. a bare
|
|
232
190
|
* stopReason 'error' with no content) — the caller skips repeat detection in
|
|
233
|
-
* that case (the stateless 0-token signal handles bare errors).
|
|
191
|
+
* that case (the stateless 0-token signal handles bare errors).
|
|
192
|
+
*
|
|
193
|
+
* R9: when the joined text is empty AND stopReason is 'error' AND usage is
|
|
194
|
+
* present with 0 total tokens, returns the constant "bare-0-token-error" so
|
|
195
|
+
* the repeat detector can corroborate bare 0-token errors (first turn is
|
|
196
|
+
* transient; repeated occurrences upgrade to poisoned at threshold). Bare
|
|
197
|
+
* 'error' WITHOUT usage keeps returning '' (absent usage = unknown; mid-
|
|
198
|
+
* response deaths stay out of repeat tracking). */
|
|
199
|
+
/** R12: normalize volatile tokens in an already-lowercased error string so that
|
|
200
|
+
* equivalent errors with varied model aliases, IPs, hex request-ids, retry
|
|
201
|
+
* counts, and long numeric ids produce the same signature. Order matters.
|
|
202
|
+
* 3-digit HTTP status codes (429/500/502/etc.) survive — the long-number rule
|
|
203
|
+
* only covers 4+ digits. Invariant: normalizeVolatileTokens never empties a
|
|
204
|
+
* non-empty string (every regex replaces with a non-empty literal token). */
|
|
205
|
+
function normalizeVolatileTokens(s) {
|
|
206
|
+
let n = s;
|
|
207
|
+
n = n.replace(/\b\d{1,3}(\.\d{1,3}){3}(:\d+)?\b/g, '<ip>'); // 1. IP:port / bare IPv4
|
|
208
|
+
n = n.replace(/\b[\w-]+(\/[\w.:+-]+)+/g, '<model>'); // 2. slash-separated paths
|
|
209
|
+
n = n.replace(/\b[0-9a-f]{8,}\b/g, '<hex>'); // 3. hex ids (8+ chars)
|
|
210
|
+
n = n.replace(/\b\d{4,}\b/g, '<n>'); // 4. long numbers (4+ digits)
|
|
211
|
+
n = n.replace(/\bafter \d+ attempts?\b/g, 'after <n> attempts'); // 5. attempt phrasing
|
|
212
|
+
n = n.replace(/\s+/g, ' ').trim(); // 6. collapse whitespace
|
|
213
|
+
return n;
|
|
214
|
+
}
|
|
234
215
|
export function extractErrorSignature(message) {
|
|
235
216
|
if (typeof message === 'string')
|
|
236
|
-
return message.toLowerCase().trim();
|
|
217
|
+
return normalizeVolatileTokens(message.toLowerCase().trim());
|
|
237
218
|
if (!message || typeof message !== 'object')
|
|
238
219
|
return '';
|
|
239
220
|
const m = message;
|
|
@@ -260,5 +241,14 @@ export function extractErrorSignature(message) {
|
|
|
260
241
|
parts.push(JSON.stringify(err));
|
|
261
242
|
}
|
|
262
243
|
}
|
|
263
|
-
|
|
244
|
+
const joined = parts.join(' ').toLowerCase().trim();
|
|
245
|
+
const normalized = normalizeVolatileTokens(joined);
|
|
246
|
+
if (normalized === '' &&
|
|
247
|
+
m.stopReason === 'error' &&
|
|
248
|
+
m.usage &&
|
|
249
|
+
typeof m.usage === 'object' &&
|
|
250
|
+
(m.usage.inputTokens ?? 0) + (m.usage.outputTokens ?? 0) === 0) {
|
|
251
|
+
return 'bare-0-token-error';
|
|
252
|
+
}
|
|
253
|
+
return normalized;
|
|
264
254
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { appendRawTranscript } from "../../src/store/sqlite.js";
|
|
2
|
+
import { computeContentDigest } from "../../src/dedup/digest.js";
|
|
3
|
+
import { ensureHwmTable, readHwm, writeHwm, dropHwm, } from "./context-hwm.js";
|
|
4
|
+
/**
|
|
5
|
+
* Append incoming messages to raw_transcript with HWM incremental support.
|
|
6
|
+
*
|
|
7
|
+
* On fork/rewind (shorter message list or boundary hash mismatch), the HWM
|
|
8
|
+
* is dropped and all messages are reprocessed.
|
|
9
|
+
*/
|
|
10
|
+
export function appendMirrorMessages(db, messages, sessionId, epochId, currentTurn) {
|
|
11
|
+
ensureHwmTable(db);
|
|
12
|
+
const hwm = readHwm(db, sessionId);
|
|
13
|
+
let tailStart = 0;
|
|
14
|
+
let needDrop = false;
|
|
15
|
+
if (hwm) {
|
|
16
|
+
if (messages.length <= hwm.lastSeq) {
|
|
17
|
+
// Session was shortened (fork/rewind/clear) — full reprocess.
|
|
18
|
+
needDrop = true;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Verify boundary integrity: hash of message at hwm.lastSeq-1
|
|
22
|
+
// must match the stored hash.
|
|
23
|
+
const boundaryMsg = messages[hwm.lastSeq - 1];
|
|
24
|
+
const boundaryHash = contentHashOf(boundaryMsg.content);
|
|
25
|
+
if (boundaryHash === hwm.lastContentHash) {
|
|
26
|
+
tailStart = hwm.lastSeq;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
needDrop = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (needDrop) {
|
|
34
|
+
dropHwm(db, sessionId);
|
|
35
|
+
}
|
|
36
|
+
// Process tail (or all if tailStart === 0).
|
|
37
|
+
let rowsProcessed = 0;
|
|
38
|
+
for (let i = tailStart; i < messages.length; i++) {
|
|
39
|
+
const raw = toRawRow(messages[i], sessionId, epochId, currentTurn);
|
|
40
|
+
if (raw) {
|
|
41
|
+
appendRawTranscript(db, raw);
|
|
42
|
+
rowsProcessed++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Write new HWM.
|
|
46
|
+
if (messages.length > 0) {
|
|
47
|
+
const lastMsg = messages[messages.length - 1];
|
|
48
|
+
writeHwm(db, {
|
|
49
|
+
sessionId,
|
|
50
|
+
lastSeq: messages.length,
|
|
51
|
+
lastContentHash: contentHashOf(lastMsg.content),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { rowsProcessed };
|
|
55
|
+
}
|
|
56
|
+
/** Single rule for hashing message content on BOTH sides of the HWM
|
|
57
|
+
* (write + boundary check) — a mismatch here silently defeats the
|
|
58
|
+
* incremental path (every event would full-reprocess). null/undefined → "". */
|
|
59
|
+
function contentHashOf(content) {
|
|
60
|
+
if (content == null)
|
|
61
|
+
return "";
|
|
62
|
+
const bytes = typeof content === "string"
|
|
63
|
+
? content
|
|
64
|
+
: JSON.stringify(canonicalize(content));
|
|
65
|
+
return computeContentDigest(bytes).contentHash;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Canonicalize a value for deterministic JSON serialization (keys sorted).
|
|
69
|
+
* This is the same logic as the inline function in context-handler.ts.
|
|
70
|
+
*/
|
|
71
|
+
function canonicalize(value) {
|
|
72
|
+
if (value === null || value === undefined)
|
|
73
|
+
return value;
|
|
74
|
+
if (typeof value !== "object")
|
|
75
|
+
return value;
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.map(canonicalize);
|
|
78
|
+
const sorted = Object.keys(value).sort();
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const k of sorted) {
|
|
81
|
+
out[k] = canonicalize(value[k]);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Convert an AgentMessage to a RawTranscriptRow (same logic as toRawTranscriptRow in context-handler.ts). */
|
|
86
|
+
function toRawRow(msg, sessionId, epochId, currentTurn) {
|
|
87
|
+
const m = msg;
|
|
88
|
+
const content = m.content;
|
|
89
|
+
if (content == null || content === "")
|
|
90
|
+
return null;
|
|
91
|
+
const contentBytes = typeof content === "string"
|
|
92
|
+
? content
|
|
93
|
+
: JSON.stringify(canonicalize(content));
|
|
94
|
+
return {
|
|
95
|
+
contentHash: computeContentDigest(contentBytes).contentHash,
|
|
96
|
+
sessionId,
|
|
97
|
+
seq: 0, // assigned by appendRawTranscript
|
|
98
|
+
role: m.role ?? "unknown",
|
|
99
|
+
contentBytes,
|
|
100
|
+
toolName: m.toolName ?? null,
|
|
101
|
+
messageTimestamp: m.timestamp ?? null,
|
|
102
|
+
checkpointEpoch: epochId,
|
|
103
|
+
turnIndex: currentTurn ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Re-export for convenience when the handler only needs the table ensure.
|
|
107
|
+
export { ensureHwmTable, readHwm, writeHwm, dropHwm };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outage-advisor.ts — R10: calm "provider outage" advisory.
|
|
3
|
+
*
|
|
4
|
+
* When a provider/router flaps (transient errors: timeouts, 5xx, 429s),
|
|
5
|
+
* the extension retries silently and eventually goes quiet after caps. This
|
|
6
|
+
* module sends ONE user-facing advisory per outage episode so the user knows
|
|
7
|
+
* to wait rather than running /clear.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from the poisoned-context /clear advise: the outage advisory is
|
|
10
|
+
* for transient errors where the user's context is fine.
|
|
11
|
+
*
|
|
12
|
+
* PREVENT-PI-003: sends via safeSendUserMessage (user-role only).
|
|
13
|
+
* PREVENT-PI-004: local ctx call, no network.
|
|
14
|
+
*/
|
|
15
|
+
import { safeSendUserMessage } from "./send-safe.js";
|
|
16
|
+
/**
|
|
17
|
+
* Check the provider-outage advisory condition and fire once per episode.
|
|
18
|
+
*
|
|
19
|
+
* Called from the transient/permanent retry branch of the turn_end handler,
|
|
20
|
+
* AFTER consecutiveErrors is incremented.
|
|
21
|
+
*
|
|
22
|
+
* @param effectiveCategory — must be "transient" for the advisory to fire.
|
|
23
|
+
* @param runtime — the live MegaRuntime (mutated on advisory fire).
|
|
24
|
+
* @param pi — pi ExtensionAPI for safeSendUserMessage.
|
|
25
|
+
* @param config — providerOutageAdviseThreshold config.
|
|
26
|
+
* @param detail — R11: optional signal + rawText for forensics.
|
|
27
|
+
*/
|
|
28
|
+
export async function maybeSendProviderOutageAdvisory(effectiveCategory, runtime, pi, config, detail) {
|
|
29
|
+
if (effectiveCategory !== "transient")
|
|
30
|
+
return;
|
|
31
|
+
if (config.providerOutageAdviseThreshold <= 0)
|
|
32
|
+
return;
|
|
33
|
+
if (runtime.rt.providerOutageAdvised)
|
|
34
|
+
return;
|
|
35
|
+
if (runtime.rt.consecutiveErrors < config.providerOutageAdviseThreshold)
|
|
36
|
+
return;
|
|
37
|
+
runtime.rt.providerOutageAdvised = true;
|
|
38
|
+
runtime.dashboard.event("provider_outage_advised", {
|
|
39
|
+
consecutiveErrors: runtime.rt.consecutiveErrors,
|
|
40
|
+
turnIndex: runtime.currentTurn,
|
|
41
|
+
sessionId: runtime.rt.sessionId,
|
|
42
|
+
...(detail?.signal !== undefined ? { signal: detail.signal } : {}),
|
|
43
|
+
...(detail?.rawText !== undefined ? { rawText: detail.rawText } : {}),
|
|
44
|
+
});
|
|
45
|
+
runtime.logger.info("provider-outage-advised", {
|
|
46
|
+
sessionId: runtime.rt.sessionId,
|
|
47
|
+
consecutiveErrors: runtime.rt.consecutiveErrors,
|
|
48
|
+
turnIndex: runtime.currentTurn,
|
|
49
|
+
...(detail?.signal !== undefined ? { signal: detail.signal } : {}),
|
|
50
|
+
...(detail?.rawText !== undefined ? { rawText: detail.rawText } : {}),
|
|
51
|
+
});
|
|
52
|
+
// Advisory is dashboard-only (events tab + log). No user-visible message —
|
|
53
|
+
// injecting into the conversation scares users and triggers false-positive
|
|
54
|
+
// re-classification of the resulting turn (2026-07-31 incident).
|
|
55
|
+
if (!config.advisoryChannel) {
|
|
56
|
+
// Legacy path (flag OFF): user-role message injection.
|
|
57
|
+
await safeSendUserMessage(pi, `[mega-compact] the provider is having issues (${runtime.rt.consecutiveErrors} consecutive failures — timeouts/5xx/rate-limits). Retries are bounded and continue automatically; your context is fine — do NOT clear or reset it. Work resumes as soon as the provider recovers.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import { recordPerfSample } from "../../src/store/sqlite.js";
|
|
2
|
+
import { classifyPrefixBreak } from "../../src/prefix-break.js";
|
|
2
3
|
/** Narrow a turn_end message to its usage block when it is an assistant msg. */
|
|
3
4
|
function usageOf(msg) {
|
|
4
5
|
if (msg.role !== "assistant" || !msg.usage)
|
|
5
6
|
return null;
|
|
6
7
|
return msg.usage;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Attempt to record a prefix_break sample when a significant cache_hit_pct
|
|
11
|
+
* drop is detected between two consecutive turns.
|
|
12
|
+
*
|
|
13
|
+
* Drop threshold: 20 percentage points. When triggered, calls
|
|
14
|
+
* classifyPrefixBreak with the three candidate event timestamps from the
|
|
15
|
+
* runtime and records a `prefix_break` sample. Best-effort + non-fatal.
|
|
16
|
+
*
|
|
17
|
+
* PREVENT-PI-004: guardrails-allow PREVENT-PI-004: local SQLite write only.
|
|
18
|
+
* PREVENT-002: SQL is fully parameterized via recordPerfSample.
|
|
19
|
+
*/
|
|
20
|
+
function tryRecordPrefixBreak(stateDir, prevHitPct, currHitPct, breakAt, runtime) {
|
|
21
|
+
try {
|
|
22
|
+
if (prevHitPct - currHitPct < 20)
|
|
23
|
+
return; // no significant drop
|
|
24
|
+
const events = {
|
|
25
|
+
lastRecallAt: runtime.rt.lastInjectAt, // S53: inject fires alongside recall
|
|
26
|
+
lastCompactAt: runtime.rt.lastCompactAt,
|
|
27
|
+
lastInjectAt: runtime.rt.lastInjectAt,
|
|
28
|
+
};
|
|
29
|
+
const { cause, confidence } = classifyPrefixBreak(breakAt, events);
|
|
30
|
+
recordPerfSample(stateDir, "prefix_break", 0, {
|
|
31
|
+
cause,
|
|
32
|
+
confidence,
|
|
33
|
+
prevHitPct,
|
|
34
|
+
currHitPct,
|
|
35
|
+
breakAt,
|
|
36
|
+
}); // guardrails-allow PREVENT-PI-004: local SQLite write (loopback perf instrumentation)
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* non-fatal: prefix-break detection never blocks the agent loop */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
8
42
|
/** Register perf instrumentation handlers + start the 5s cpu/mem interval. */
|
|
9
43
|
export function registerPerfHandler(pi, runtime) {
|
|
10
44
|
// turn_start: record the wall-clock start of the turn. Using Date.now() (not
|
|
@@ -23,6 +57,7 @@ export function registerPerfHandler(pi, runtime) {
|
|
|
23
57
|
});
|
|
24
58
|
// turn_end: compute turn latency + TPS + cache hit % from the assistant
|
|
25
59
|
// message's usage block. One perf_samples row per metric per turn.
|
|
60
|
+
// S53A: tracks prevHitPct across turns to detect prefix breaks.
|
|
26
61
|
pi.on("turn_end", async (event) => {
|
|
27
62
|
try {
|
|
28
63
|
if (runtime.perfTurnStart > 0) {
|
|
@@ -36,7 +71,22 @@ export function registerPerfHandler(pi, runtime) {
|
|
|
36
71
|
recordPerfSample(runtime.currentStateDir, "tps", u.output / durSec, { outputTokens: u.output });
|
|
37
72
|
const denom = u.cacheRead + u.input + u.cacheWrite;
|
|
38
73
|
const hitPct = denom > 0 ? (u.cacheRead / denom) * 100 : 0;
|
|
39
|
-
recordPerfSample(runtime.currentStateDir, "cache_hit_pct", hitPct, {
|
|
74
|
+
recordPerfSample(runtime.currentStateDir, "cache_hit_pct", hitPct, {
|
|
75
|
+
input: u.input,
|
|
76
|
+
cacheRead: u.cacheRead,
|
|
77
|
+
cacheWrite: u.cacheWrite,
|
|
78
|
+
modelName: runtime.currentModel?.modelName,
|
|
79
|
+
modelId: runtime.currentModel?.modelId,
|
|
80
|
+
});
|
|
81
|
+
// S53A: detect significant cache hit % drop → record prefix_break.
|
|
82
|
+
// Only trigger when we have a previous value AND the drop is >20pp.
|
|
83
|
+
if (runtime.rt._prevCacheHitPct != null &&
|
|
84
|
+
runtime.rt._prevCacheHitPct - hitPct >= 20) {
|
|
85
|
+
const breakAt = runtime.perfTurnStart;
|
|
86
|
+
tryRecordPrefixBreak(runtime.currentStateDir, runtime.rt._prevCacheHitPct, hitPct, breakAt, runtime);
|
|
87
|
+
}
|
|
88
|
+
// Stamp for the next turn's comparison (even when no break fires).
|
|
89
|
+
runtime.rt._prevCacheHitPct = hitPct;
|
|
40
90
|
}
|
|
41
91
|
}
|
|
42
92
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when recall blocks are staged AND the feature flag is ON. Called by the
|
|
3
|
+
* context handler to decide whether to call withRecallTail.
|
|
4
|
+
*/
|
|
5
|
+
export function stagedForTail(runtime, config) {
|
|
6
|
+
if (!config.recallTailInject)
|
|
7
|
+
return false;
|
|
8
|
+
return (runtime.pendingRecallBlock != null ||
|
|
9
|
+
runtime.pendingMemoryRecallBlock != null);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Append staged recall/memory blocks as a single user-role message at the very
|
|
13
|
+
* tail of the messages array. Returns a NEW array (does not mutate input).
|
|
14
|
+
*
|
|
15
|
+
* Sets `runtime.recallInjectedThisTurn = true` when it actually appends, so the
|
|
16
|
+
* turn_end handler (agent-handlers.ts) consumes the staged blocks only if they
|
|
17
|
+
* reached a view this turn. If a turn ends with no context event, the flag
|
|
18
|
+
* stays false and the blocks remain staged for the next turn.
|
|
19
|
+
*
|
|
20
|
+
* Stable timestamp (runtime.perfTurnStart ?? Date.now()) so replays within the
|
|
21
|
+
* turn are byte-identical (mirrors the v0.8.6 summaryAgentMsg rationale).
|
|
22
|
+
*
|
|
23
|
+
* Non-fatal: on any failure, returns messages unmodified and does NOT set the
|
|
24
|
+
* injected flag. PREVENT-PI-003: the injected message has role "user", never
|
|
25
|
+
* "system".
|
|
26
|
+
*/
|
|
27
|
+
export function withRecallTail(messages, runtime, config) {
|
|
28
|
+
if (!config.recallTailInject)
|
|
29
|
+
return messages;
|
|
30
|
+
try {
|
|
31
|
+
const blocks = [];
|
|
32
|
+
if (runtime.pendingRecallBlock != null) {
|
|
33
|
+
blocks.push(runtime.pendingRecallBlock);
|
|
34
|
+
}
|
|
35
|
+
if (runtime.pendingMemoryRecallBlock != null) {
|
|
36
|
+
blocks.push(runtime.pendingMemoryRecallBlock);
|
|
37
|
+
}
|
|
38
|
+
if (blocks.length === 0)
|
|
39
|
+
return messages;
|
|
40
|
+
const tailText = blocks.join("\n\n");
|
|
41
|
+
const timestamp = runtime.perfTurnStart ?? // stable within a turn
|
|
42
|
+
Date.now();
|
|
43
|
+
const tailMsg = {
|
|
44
|
+
role: "user",
|
|
45
|
+
content: tailText,
|
|
46
|
+
timestamp,
|
|
47
|
+
};
|
|
48
|
+
runtime.rt.recallInjectedThisTurn = true;
|
|
49
|
+
// Append at the very tail — never splits toolCall/toolResult.
|
|
50
|
+
// PREVENT-PI-001/002: a single message appended after a complete
|
|
51
|
+
// transcript prefix cannot split a toolCall/toolResult pair.
|
|
52
|
+
return [...messages, tailMsg];
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Non-fatal: leave blocks staged (flag stays unset) and return the
|
|
56
|
+
// unmodified view. The context event still proceeds normally.
|
|
57
|
+
return messages;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -149,14 +149,21 @@ export function registerSessionHandlers(pi, runtime, config) {
|
|
|
149
149
|
catch {
|
|
150
150
|
/* non-fatal: intent polling never breaks the agent loop */
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
// S53: when recallTailInject is ON, the staged blocks are injected as a
|
|
153
|
+
// user-role tail message by the context handler (recall-tail.ts), NOT
|
|
154
|
+
// here. This prepend path is the legacy behavior (flag OFF = byte-
|
|
155
|
+
// identical pre-sprint). captureModel + rewind-intent consumption above
|
|
156
|
+
// stay unconditional regardless of the flag.
|
|
157
|
+
if (!config.recallTailInject) {
|
|
158
|
+
const cpBlock = runtime.pendingRecallBlock;
|
|
159
|
+
const memBlock = runtime.pendingMemoryRecallBlock;
|
|
160
|
+
if (!cpBlock && !memBlock)
|
|
161
|
+
return;
|
|
162
|
+
runtime.pendingRecallBlock = undefined;
|
|
163
|
+
runtime.pendingMemoryRecallBlock = undefined;
|
|
164
|
+
const composed = [cpBlock, memBlock].filter(Boolean).join("\n\n");
|
|
165
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${composed}` };
|
|
166
|
+
}
|
|
160
167
|
});
|
|
161
168
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
162
169
|
runtime.setStatus(ctx, undefined);
|