pi-mega-compact 0.20.64 → 0.20.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/config/vector-cortex-enc2b.js +36 -0
- package/dist/config/vector-cortex.js +2 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes-setup-enc2b.js +77 -0
- package/dist/extensions/dashboard-server/routes-setup.js +39 -3
- package/dist/extensions/mega-compact-s38.test/_helpers.js +2 -1
- package/dist/extensions/mega-compact.test/_helpers.js +4 -1
- package/dist/extensions/mega-events/agent-handlers/agentEndHandler.js +2 -2
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry-transient.js +131 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.js +13 -137
- package/dist/extensions/mega-events/compact-handlers.js +3 -3
- package/dist/extensions/mega-events/outage-advisor.js +5 -5
- package/dist/extensions/mega-events/send-safe.js +16 -0
- package/dist/src/config/vector-cortex-enc2b.js +36 -0
- package/dist/src/config/vector-cortex.js +2 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/encoder/native-qualify-retest.js +225 -0
- package/dist/vector-cortex/encoder/native-qualify-retest.js +225 -0
- package/extensions/dashboard-client/dist/assets/{AdminTab-sjV8P5A4.js → AdminTab-B31efZ_R.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AdminTab-sjV8P5A4.js.map → AdminTab-B31efZ_R.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AreaChart-C03yuld4.js → AreaChart-BpjHNUZm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-C03yuld4.js.map → AreaChart-BpjHNUZm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-B9jtJ7ud.js → BarChart-CY2FndXC.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-B9jtJ7ud.js.map → BarChart-CY2FndXC.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-B7Eh3qfp.js → CacheTab-kMSrMD3e.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-B7Eh3qfp.js.map → CacheTab-kMSrMD3e.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDTqosv2.js → ConfigTab-BrKpv2YG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDTqosv2.js.map → ConfigTab-BrKpv2YG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-BmyQGmUa.js → EventsTab-DaClreCm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-BmyQGmUa.js.map → EventsTab-DaClreCm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-DzPqNqpm.js → HealthTab-CxKQDqF3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-DzPqNqpm.js.map → HealthTab-CxKQDqF3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-6g3d-M1p.js → MaintenanceTab-BKSlSjv0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-6g3d-M1p.js.map → MaintenanceTab-BKSlSjv0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Nk2yYPMS.js → MemoryMapTab-C2goWDt8.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Nk2yYPMS.js.map → MemoryMapTab-C2goWDt8.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsCards-BlIiuZp5.js → MetricsCards-DCPuCnrw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsCards-BlIiuZp5.js.map → MetricsCards-DCPuCnrw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MetricsTab-Bz0Mdyb_.js +2 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DfRDIAEU.js.map → MetricsTab-Bz0Mdyb_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-xkuFE5GV.js → OverviewTab-B-eZ-eDR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-xkuFE5GV.js.map → OverviewTab-B-eZ-eDR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-C6qZmmgQ.js → ReposTab-W_NKRWpk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C6qZmmgQ.js.map → ReposTab-W_NKRWpk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Bkd_In3n.js → SessionsTab-GYrtNt4_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Bkd_In3n.js.map → SessionsTab-GYrtNt4_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-_1xa6eTJ.js +3 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-_1xa6eTJ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dr0KNUed.js → TimeSavedCard-Ciu8anVG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dr0KNUed.js.map → TimeSavedCard-Ciu8anVG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnMemoryView-CjmSiRQ_.js → TurnMemoryView-BhXLIjWr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnMemoryView-CjmSiRQ_.js.map → TurnMemoryView-BhXLIjWr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-C-f3DggL.js +2 -0
- package/extensions/dashboard-client/dist/assets/{TurnsTab-JsDstDDA.js.map → TurnsTab-C-f3DggL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{VcStatusBadge-C96wfemg.js → VcStatusBadge-D97V04WE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{VcStatusBadge-C96wfemg.js.map → VcStatusBadge-D97V04WE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{VectorCortexTab-DGV3kA7X.js → VectorCortexTab-cyDJPAPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{VectorCortexTab-DGV3kA7X.js.map → VectorCortexTab-cyDJPAPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{WikiTab-EFfy5v-D.js → WikiTab-dQ96DyjP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-EFfy5v-D.js.map → WikiTab-dQ96DyjP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-CvaPx9Bu.js → button-B1beLEiV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-CvaPx9Bu.js.map → button-B1beLEiV.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-Crs5SwRQ.js → card-CK9KH_ob.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-Crs5SwRQ.js.map → card-CK9KH_ob.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{client-extra-Bd27WKSo.js → client-extra-gN7xjggX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{client-extra-Bd27WKSo.js.map → client-extra-gN7xjggX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-qnHql9Es.js → generateCategoricalChart-MB2_0u55.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-qnHql9Es.js.map → generateCategoricalChart-MB2_0u55.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B4twCFnn.js → index-CKVd_Ym_.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-B4twCFnn.js.map → index-CKVd_Ym_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-BXutQI5d.js → switch-CswmYrO_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-BXutQI5d.js.map → switch-CswmYrO_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-uE-OSaba.js → toggle-CMepFsf0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-uE-OSaba.js.map → toggle-CMepFsf0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BRgPAqeS.js → useSSE-Bl55LQFZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BRgPAqeS.js.map → useSSE-Bl55LQFZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/tabs/SetupTab/CortexRetestCard.tsx +104 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/CortexSetup.tsx +2 -0
- package/extensions/dashboard-server/api-contracts/setup.ts +30 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-setup-enc2b.ts +82 -0
- package/extensions/dashboard-server/routes-setup.ts +39 -3
- package/extensions/mega-compact-s38.test/_helpers.ts +1 -1
- package/extensions/mega-compact.test/_helpers.ts +3 -1
- package/extensions/mega-events/agent-handlers/agentEndHandler.ts +2 -2
- package/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry-transient.ts +170 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.ts +12 -147
- package/extensions/mega-events/compact-handlers.ts +3 -3
- package/extensions/mega-events/outage-advisor.ts +5 -5
- package/extensions/mega-events/send-safe.ts +35 -4
- package/package.json +1 -1
- package/src/config/vector-cortex-enc2b.ts +38 -0
- package/src/config/vector-cortex.ts +2 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/encoder/native-qualify-retest.ts +264 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-DfRDIAEU.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-CEXL71VP.js +0 -3
- package/extensions/dashboard-client/dist/assets/SetupTab-CEXL71VP.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-JsDstDDA.js +0 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-setup-enc2b.ts — ENC-2b native onnxruntime
|
|
3
|
+
* qualification retest (reader-only probe) + POST retest action.
|
|
4
|
+
*
|
|
5
|
+
* Extracted out of routes-setup.ts (which hovers at the 300-line source soft
|
|
6
|
+
* cap, mirroring the ENC-1b/ENC-2a sibling extracts) so the ENC-2b additive
|
|
7
|
+
* GET field — the native qualification retest result + effective backend —
|
|
8
|
+
* lives in a sibling impl file and routes-setup.ts stays a thin host.
|
|
9
|
+
*
|
|
10
|
+
* The retest's job is RE-QUALIFICATION: when the operator installed
|
|
11
|
+
* onnxruntime-node via the ENC-2a guide, the encoder read path does not
|
|
12
|
+
* automatically re-evaluate the qualification verdict. This sibling surface
|
|
13
|
+
* runs `runNativeRetest` (a bounded, reader-only, local probe) and:
|
|
14
|
+
*
|
|
15
|
+
* - surfaces `nativeOrtRetestResult` + `nativeOrtBackendEffective` on the GET
|
|
16
|
+
* when the flag is on AND a binding is installed — absent otherwise
|
|
17
|
+
* (byte-identical to the ENC-2a-era shape on flag-off/none);
|
|
18
|
+
* - handles the POST `nativeOrtRetest` key (`false` → 400
|
|
19
|
+
* `retest_rejected_false_nothing_to_do`; `true` → run + echo the fresh
|
|
20
|
+
* result; flag-off → the key is unrecognized, byte-identical predecessor).
|
|
21
|
+
*
|
|
22
|
+
* `nativeOrtBackendEffective` is `"native"` only when the retest verdict is
|
|
23
|
+
* `qualified`; any `degraded`/`failed` verdict keeps the backend `"wasm"` — the
|
|
24
|
+
* runtime never silently switches to a backend whose retest failed.
|
|
25
|
+
*
|
|
26
|
+
* Guardrails: PREVENT-PI-004 (local filesystem + in-process binding load only,
|
|
27
|
+
* zero network — the retest NEVER fetches), PREVENT-011 (no `any`).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { ENC_2B_ENABLED } from "../../src/config/vector-cortex.js";
|
|
31
|
+
import {
|
|
32
|
+
runNativeRetest,
|
|
33
|
+
type RetestResult,
|
|
34
|
+
} from "../../src/vector-cortex/encoder/native-qualify-retest.js";
|
|
35
|
+
import type { SetupConfigureRequest } from "./api-contracts/setup.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the ENC-2b additive GET fields. Returns BOTH fields when the flag is
|
|
39
|
+
* on AND a native binding is installed (the retest ran); returns `{}` on
|
|
40
|
+
* flag-off AND when no binding is installed — both fields are OMITTED (absent,
|
|
41
|
+
* not null), so the GET stays byte-identical to the ENC-2a-era shape and the
|
|
42
|
+
* client hides the retest card. Flag-off omits, never nulls: the ENC-2a-era
|
|
43
|
+
* host spreads `...{}` so no new key appears. Mirror of the ENC-2a/ENC-1b
|
|
44
|
+
* sibling idiom; async because the retest loads the binding in-process.
|
|
45
|
+
*/
|
|
46
|
+
export async function readEnc2bRetest(stateDir: string): Promise<{
|
|
47
|
+
nativeOrtRetestResult?: RetestResult;
|
|
48
|
+
nativeOrtBackendEffective?: "native" | "wasm";
|
|
49
|
+
}> {
|
|
50
|
+
if (!ENC_2B_ENABLED()) return {};
|
|
51
|
+
const result = await runNativeRetest(stateDir);
|
|
52
|
+
if (result === null) return {};
|
|
53
|
+
return {
|
|
54
|
+
nativeOrtRetestResult: result,
|
|
55
|
+
nativeOrtBackendEffective: result.verdict === "qualified" ? "native" : "wasm",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ENC-2b POST retest-action key handling (flag-gated, additive).
|
|
61
|
+
*
|
|
62
|
+
* `true` → { action: "run" } (the host runs the retest and returns the result)
|
|
63
|
+
* `false` → { action: "reject" } (400 `retest_rejected_false_nothing_to_do`)
|
|
64
|
+
* other → null (key absent or flag off — falls through unrecognized,
|
|
65
|
+
* byte-identical ENC-2a-era predecessor).
|
|
66
|
+
*/
|
|
67
|
+
export function enc2bRetestRequest(body: SetupConfigureRequest): "run" | "reject" | null {
|
|
68
|
+
if (!ENC_2B_ENABLED()) return null;
|
|
69
|
+
if (body.nativeOrtRetest === true) return "run";
|
|
70
|
+
if (body.nativeOrtRetest === false) return "reject";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run the ENC-2b retest now (bounded, synchronous on the request) and return
|
|
76
|
+
* the fresh result. Flag-off or no binding → null. Used by the POST "run"
|
|
77
|
+
* branch; the result is returned on the response body, never persisted.
|
|
78
|
+
*/
|
|
79
|
+
export async function runEnc2bRetest(stateDir: string): Promise<RetestResult | null> {
|
|
80
|
+
if (!ENC_2B_ENABLED()) return null;
|
|
81
|
+
return runNativeRetest(stateDir);
|
|
82
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
enc2BudgetValidateCombined,
|
|
23
23
|
} from "./routes-setup-enc2budget.js";
|
|
24
24
|
import { readEnc2aGuide, enc2aGuideRequest } from "./routes-setup-enc2a.js";
|
|
25
|
+
import { readEnc2bRetest, enc2bRetestRequest, runEnc2bRetest } from "./routes-setup-enc2b.js";
|
|
25
26
|
import { writeEmbedderEnv } from "./routes-setup-env-upsert.js";
|
|
26
27
|
import {
|
|
27
28
|
detectOllama,
|
|
@@ -94,7 +95,7 @@ export function handleSetupStatus(
|
|
|
94
95
|
// ENC-1a (flag-gated, additive): echo the persisted endpoint URL and an
|
|
95
96
|
// embeddingApiKeySet boolean ONLY — the raw API key is never returned. When
|
|
96
97
|
// the flag is off (or neither is set) both fields are simply omitted.
|
|
97
|
-
const
|
|
98
|
+
const base: SetupStatusResponse = {
|
|
98
99
|
currentEmbedder: active,
|
|
99
100
|
configuredEmbedder: configured.embedder,
|
|
100
101
|
configuredUrl: configured.url,
|
|
@@ -109,9 +110,20 @@ export function handleSetupStatus(
|
|
|
109
110
|
...(enc2a.guide !== null ? { nativeOrtInstallGuide: enc2a.guide } : {}),
|
|
110
111
|
...(enc2a.installedVersion !== null ? { nativeOrtInstalledVersion: enc2a.installedVersion } : {}),
|
|
111
112
|
};
|
|
113
|
+
// ENC-2b (flag-gated, additive): the native qualification retest fields. The
|
|
114
|
+
// retest loads the on-disk binding in-process (bounded, never a fetch), so
|
|
115
|
+
// readEnc2bRetest is async; the handler returns true synchronously for the
|
|
116
|
+
// dispatch and writes the response after the await. Flag-off/none mutates
|
|
117
|
+
// the body additively and stays byte-identical to the ENC-2a-era shape.
|
|
112
118
|
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
readEnc2bRetest(ctx.stateDir).then((enc2b) => {
|
|
120
|
+
const body: SetupStatusResponse = { ...base, ...enc2b };
|
|
121
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
122
|
+
res.end(JSON.stringify(body));
|
|
123
|
+
}).catch(() => {
|
|
124
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
125
|
+
res.end(JSON.stringify(base));
|
|
126
|
+
});
|
|
115
127
|
return true;
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -248,6 +260,30 @@ export function handleSetupConfigure(
|
|
|
248
260
|
res.end(JSON.stringify({ nativeOrtInstallGuide: readEnc2aGuide(ctx.stateDir).guide }));
|
|
249
261
|
return;
|
|
250
262
|
}
|
|
263
|
+
// ENC-2b retest-action key (flag-gated, additive): `false` → 400 (an
|
|
264
|
+
// operator-driven no-op — nothing to retest), `true` → run the bounded
|
|
265
|
+
// retest synchronously and return the fresh result. Flag-off: the key
|
|
266
|
+
// falls through unrecognized (byte-identical ENC-2a-era predecessor).
|
|
267
|
+
// The retest never fetches — the binding is on disk from the ENC-2a
|
|
268
|
+
// install (PREVENT-PI-004 clean).
|
|
269
|
+
const retestReq = enc2bRetestRequest(body);
|
|
270
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
271
|
+
if (retestReq === "reject") {
|
|
272
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
273
|
+
res.end(JSON.stringify({ error: "retest_rejected_false_nothing_to_do" }));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (retestReq === "run") {
|
|
277
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
278
|
+
runEnc2bRetest(ctx.stateDir).then((fresh) => {
|
|
279
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
280
|
+
res.end(JSON.stringify({ nativeOrtRetestResult: fresh }));
|
|
281
|
+
}).catch(() => {
|
|
282
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
283
|
+
res.end(JSON.stringify({ error: "retest_failed" }));
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
251
287
|
if (tryEnc1aConfigure(body, res, ctx)) return;
|
|
252
288
|
if (tryEnc1bConfigure(body, res, ctx)) return; // pure ENC-1b configure (dim/headers/allow-remote/native)
|
|
253
289
|
// ENC-1b combined-payload validation: when a payload carries a valid
|
|
@@ -122,7 +122,7 @@ export function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } =
|
|
|
122
122
|
registerCommand: () => {}, registerTool: () => {}, registerShortcut: () => {},
|
|
123
123
|
registerFlag: () => {}, getFlag: () => undefined,
|
|
124
124
|
registerMessageRenderer: () => {}, registerEntryRenderer: () => {},
|
|
125
|
-
sendMessage: () => {}, sendUserMessage: (m: string) => { sendUserMessages.push(m); },
|
|
125
|
+
sendMessage: (m: { content?: string }) => { if (typeof m?.content === "string") sendUserMessages.push(m.content); }, sendUserMessage: (m: string) => { sendUserMessages.push(m); },
|
|
126
126
|
appendEntry: (t: string, d: any) => appended.push({ t, d }),
|
|
127
127
|
setSessionName: () => {}, getSessionName: () => undefined, setLabel: () => {},
|
|
128
128
|
exec: async () => ({ stdout: "", stderr: "", code: 0 }),
|
|
@@ -197,7 +197,9 @@ export function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } =
|
|
|
197
197
|
getFlag: () => undefined,
|
|
198
198
|
registerMessageRenderer: () => {},
|
|
199
199
|
registerEntryRenderer: () => {},
|
|
200
|
-
sendMessage: (
|
|
200
|
+
sendMessage: (m: { content?: string }) => {
|
|
201
|
+
if (typeof m?.content === "string") sendUserMessages.push(m.content);
|
|
202
|
+
},
|
|
201
203
|
sendUserMessage: (m: string) => {
|
|
202
204
|
sendUserMessages.push(m);
|
|
203
205
|
},
|
|
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
10
10
|
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
11
11
|
import { piCompactWouldNoop } from "../../mega-pipeline.js";
|
|
12
12
|
import type { MegaConfig } from "../../mega-config.js";
|
|
13
|
-
import {
|
|
13
|
+
import { safeSendInvisibleMessage } from "../send-safe.js";
|
|
14
14
|
|
|
15
15
|
/** Handle the `agent_end` pi event. Non-fatal end-to-end. */
|
|
16
16
|
export async function handleAgentEnd(
|
|
@@ -228,7 +228,7 @@ export async function handleAgentEnd(
|
|
|
228
228
|
lengthStop && !didDurableTrim
|
|
229
229
|
? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
|
|
230
230
|
: "[mega-compact] continue from the compacted context above.";
|
|
231
|
-
await
|
|
231
|
+
await safeSendInvisibleMessage(pi, nudgeMsg);
|
|
232
232
|
}
|
|
233
233
|
} catch {
|
|
234
234
|
/* non-fatal: a failed nudge never blocks */
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnEndHandler/errorRetry-transient.ts — transient/permanent retry branch.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from errorRetry.ts (delegate-shell split) to keep every source
|
|
5
|
+
* file under the extensions soft limit. Contains the S38.6 circuit breaker,
|
|
6
|
+
* R10 provider-outage advisory, per-burst max, R2 session cap, R1 in-flight
|
|
7
|
+
* dedup + gating backoff, and the actual retry nudge send.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { MegaRuntime } from "../../../mega-runtime.js";
|
|
11
|
+
import type { MegaConfig } from "../../../mega-config.js";
|
|
12
|
+
import { errorRetryBackoffMs } from "../../error-classifier.js";
|
|
13
|
+
import { safeSendInvisibleMessage } from "../../send-safe.js";
|
|
14
|
+
import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
|
|
15
|
+
import type { TurnEndEvent } from "./event.js";
|
|
16
|
+
|
|
17
|
+
/** Context needed from the caller's scope for the transient/permanent branch. */
|
|
18
|
+
export interface TransientRetryContext {
|
|
19
|
+
readonly effectiveCategory: string;
|
|
20
|
+
readonly detail: { signal?: string };
|
|
21
|
+
readonly errSig: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Handle the transient or permanent error-retry path. Non-fatal. */
|
|
25
|
+
export async function transientRetry(
|
|
26
|
+
event: TurnEndEvent,
|
|
27
|
+
_ctx: ExtensionContext,
|
|
28
|
+
pi: ExtensionAPI,
|
|
29
|
+
runtime: MegaRuntime,
|
|
30
|
+
config: MegaConfig,
|
|
31
|
+
tc: TransientRetryContext,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { effectiveCategory, detail, errSig } = tc;
|
|
34
|
+
// S38.7: hard-stop switch — bypass ALL retry logic when set.
|
|
35
|
+
if (config.errorRetryHardStop) {
|
|
36
|
+
runtime.rt.errorRetryCount = 0;
|
|
37
|
+
runtime.dashboard.event("error_retry_disabled", {
|
|
38
|
+
category: effectiveCategory,
|
|
39
|
+
turnIndex: event.turnIndex,
|
|
40
|
+
reason: "hard-stop",
|
|
41
|
+
});
|
|
42
|
+
return; // early exit — no retry
|
|
43
|
+
}
|
|
44
|
+
// S38.6: circuit-breaker — stop retrying after too many consecutive errors.
|
|
45
|
+
runtime.rt.consecutiveErrors++;
|
|
46
|
+
// R10: send calm "provider outage" advisory once per episode.
|
|
47
|
+
await maybeSendProviderOutageAdvisory(
|
|
48
|
+
effectiveCategory, runtime, pi, config,
|
|
49
|
+
{ signal: detail.signal, rawText: (errSig || '').slice(0, 500) },
|
|
50
|
+
);
|
|
51
|
+
if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
|
|
52
|
+
runtime.dashboard.event("error_retry_circuit_open", {
|
|
53
|
+
consecutive: runtime.rt.consecutiveErrors,
|
|
54
|
+
max: config.maxConsecutiveErrors,
|
|
55
|
+
turnIndex: event.turnIndex,
|
|
56
|
+
});
|
|
57
|
+
runtime.logger.warn("error-retry-circuit-open", {
|
|
58
|
+
sessionId: runtime.rt.sessionId,
|
|
59
|
+
consecutive: runtime.rt.consecutiveErrors,
|
|
60
|
+
max: config.maxConsecutiveErrors,
|
|
61
|
+
});
|
|
62
|
+
return; // early exit — circuit breaker tripped
|
|
63
|
+
}
|
|
64
|
+
const max =
|
|
65
|
+
effectiveCategory === "transient"
|
|
66
|
+
? config.autoRetryTransientMax
|
|
67
|
+
: config.autoRetryPermanentMax;
|
|
68
|
+
// max === 0 disables the category entirely (revert to S28-only).
|
|
69
|
+
if (max <= 0) {
|
|
70
|
+
runtime.rt.errorRetryCount = 0;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
runtime.rt.errorRetryCount++;
|
|
74
|
+
if (runtime.rt.errorRetryCount > max) {
|
|
75
|
+
// Exhausted — surface the error, reset for the next burst.
|
|
76
|
+
runtime.dashboard.event("error_retry_exhausted", {
|
|
77
|
+
category: effectiveCategory,
|
|
78
|
+
count: runtime.rt.errorRetryCount,
|
|
79
|
+
max,
|
|
80
|
+
turnIndex: event.turnIndex,
|
|
81
|
+
});
|
|
82
|
+
runtime.logger.info("error-retry-exhausted", {
|
|
83
|
+
sessionId: runtime.rt.sessionId,
|
|
84
|
+
category: effectiveCategory,
|
|
85
|
+
count: runtime.rt.errorRetryCount,
|
|
86
|
+
max,
|
|
87
|
+
});
|
|
88
|
+
runtime.rt.errorRetryCount = 0;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// R2: session-global cap — total S38 nudges per session across ALL bursts.
|
|
92
|
+
// Independent of the per-burst max and the circuit breaker. Hitting it is
|
|
93
|
+
// terminal for the session: log + dashboard event, stop nudging. `0` disables.
|
|
94
|
+
if (
|
|
95
|
+
config.errorRetrySessionMax > 0 &&
|
|
96
|
+
runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
|
|
97
|
+
) {
|
|
98
|
+
runtime.dashboard.event("error_retry_session_exhausted", {
|
|
99
|
+
count: runtime.rt.errorRetrySessionCount,
|
|
100
|
+
max: config.errorRetrySessionMax,
|
|
101
|
+
category: effectiveCategory,
|
|
102
|
+
turnIndex: event.turnIndex,
|
|
103
|
+
});
|
|
104
|
+
runtime.logger.warn("error-retry-session-exhausted", {
|
|
105
|
+
sessionId: runtime.rt.sessionId,
|
|
106
|
+
count: runtime.rt.errorRetrySessionCount,
|
|
107
|
+
max: config.errorRetrySessionMax,
|
|
108
|
+
category: effectiveCategory,
|
|
109
|
+
});
|
|
110
|
+
runtime.rt.errorRetryCount = 0;
|
|
111
|
+
return; // terminal for the session — no nudge
|
|
112
|
+
}
|
|
113
|
+
// R1: in-flight nudge dedup — a nudge queued via deliverAs:'followUp' must
|
|
114
|
+
// not be re-sent until consumed by an actual new agent turn (turn_start
|
|
115
|
+
// resets retryNudgePending). Without this, a fast-erroring provider + a
|
|
116
|
+
// per-turn nudge → N nudges queue up and pi dispatches N retry turns, each
|
|
117
|
+
// re-submitting the same failing prompt (the 2026-07-28 incident).
|
|
118
|
+
if (runtime.rt.retryNudgePending) {
|
|
119
|
+
runtime.dashboard.event("error_retry_dedup_skip", {
|
|
120
|
+
category: effectiveCategory,
|
|
121
|
+
count: runtime.rt.errorRetryCount,
|
|
122
|
+
max,
|
|
123
|
+
turnIndex: event.turnIndex,
|
|
124
|
+
});
|
|
125
|
+
return; // pending nudge not yet consumed — skip
|
|
126
|
+
}
|
|
127
|
+
// R1: gating backoff — errorRetryUntil is GATING. A nudge cannot fire before
|
|
128
|
+
// the previous backoff elapses. Paces retries (5s/10s/20s/30s default).
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
if (now < runtime.rt.errorRetryUntil) {
|
|
131
|
+
runtime.dashboard.event("error_retry_backoff_skip", {
|
|
132
|
+
category: effectiveCategory,
|
|
133
|
+
count: runtime.rt.errorRetryCount,
|
|
134
|
+
max,
|
|
135
|
+
turnIndex: event.turnIndex,
|
|
136
|
+
});
|
|
137
|
+
return; // backoff not elapsed — skip
|
|
138
|
+
}
|
|
139
|
+
// Fire the retry nudge. Set pending (R1 dedup) + backoff (R1 pacing) +
|
|
140
|
+
// session count (R2 cap) BEFORE the await so a re-entrant turn_end during
|
|
141
|
+
// the send can't double-fire.
|
|
142
|
+
runtime.rt.retryNudgePending = true;
|
|
143
|
+
runtime.rt.lastErrorRetryAt = now;
|
|
144
|
+
runtime.rt.errorRetryUntil =
|
|
145
|
+
now +
|
|
146
|
+
errorRetryBackoffMs(
|
|
147
|
+
runtime.rt.errorRetryCount,
|
|
148
|
+
config.errorRetryBackoffMs,
|
|
149
|
+
);
|
|
150
|
+
runtime.rt.errorRetrySessionCount++;
|
|
151
|
+
runtime.dashboard.event("error_retry", {
|
|
152
|
+
category: effectiveCategory,
|
|
153
|
+
count: runtime.rt.errorRetryCount,
|
|
154
|
+
max,
|
|
155
|
+
turnIndex: event.turnIndex,
|
|
156
|
+
});
|
|
157
|
+
runtime.logger.info("error-retry", {
|
|
158
|
+
sessionId: runtime.rt.sessionId,
|
|
159
|
+
category: effectiveCategory,
|
|
160
|
+
count: runtime.rt.errorRetryCount,
|
|
161
|
+
max,
|
|
162
|
+
});
|
|
163
|
+
// PREVENT-PI-003: user-role only (queued + catch-guarded).
|
|
164
|
+
// Invisible: display:false so the retry trigger fires without
|
|
165
|
+
// rendering "Follow-up:" spam in the conversation UI.
|
|
166
|
+
await safeSendInvisibleMessage(
|
|
167
|
+
pi,
|
|
168
|
+
"[mega-compact] the last turn ended with an error; please retry.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -17,12 +17,11 @@ import type { MegaConfig } from "../../../mega-config.js";
|
|
|
17
17
|
import {
|
|
18
18
|
classifyError,
|
|
19
19
|
classifyErrorDetailed,
|
|
20
|
-
errorRetryBackoffMs,
|
|
21
20
|
extractErrorSignature,
|
|
22
21
|
isKnownRetryableTransient,
|
|
23
22
|
} from "../../error-classifier.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
23
|
+
import { safeSendInvisibleMessage } from "../../send-safe.js";
|
|
24
|
+
import { transientRetry } from "./errorRetry-transient.js";
|
|
26
25
|
import type { TurnEndEvent } from "./event.js";
|
|
27
26
|
|
|
28
27
|
/** S38: broader error-retry safety net. Non-fatal end-to-end. */
|
|
@@ -232,7 +231,10 @@ export async function errorRetry(
|
|
|
232
231
|
// Default ON (advisoryChannel=true): dashboard-only, no user message.
|
|
233
232
|
if (!config.advisoryChannel) {
|
|
234
233
|
// PREVENT-PI-003: user-role sendUserMessage only.
|
|
235
|
-
|
|
234
|
+
// Invisible: display:false — the dashboard event + log already
|
|
235
|
+
// capture the poisoned-context signal; the user does not need a
|
|
236
|
+
// visible "Follow-up:" message.
|
|
237
|
+
await safeSendInvisibleMessage(
|
|
236
238
|
pi,
|
|
237
239
|
"[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
240
|
);
|
|
@@ -270,149 +272,12 @@ export async function errorRetry(
|
|
|
270
272
|
}
|
|
271
273
|
} else {
|
|
272
274
|
// (5) transient or permanent — retry with exponential backoff.
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
}
|
|
275
|
+
// Delegated to errorRetry-transient.ts (delegate-shell split).
|
|
276
|
+
await transientRetry(event, ctx, pi, runtime, config, {
|
|
277
|
+
effectiveCategory,
|
|
278
|
+
detail,
|
|
279
|
+
errSig,
|
|
280
|
+
});
|
|
416
281
|
}
|
|
417
282
|
}
|
|
418
283
|
} catch {
|
|
@@ -22,7 +22,7 @@ import type { MegaConfig } from "../mega-config.js";
|
|
|
22
22
|
import { recordScore, getDedupStats } from "../../src/store/sqlite.js";
|
|
23
23
|
import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
|
|
24
24
|
import { resolveRepoRoot } from "../mega-config.js";
|
|
25
|
-
import {
|
|
25
|
+
import { safeSendInvisibleMessage } from "./send-safe.js";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Build a minimal fallback compaction so pi never runs its throwing compact().
|
|
@@ -59,7 +59,7 @@ function fallbackCompaction(
|
|
|
59
59
|
* Debounced resume-nudge: restart the agent loop after a compaction (which
|
|
60
60
|
* may have stopped it). Idempotent — one nudge per 30s, never blocks.
|
|
61
61
|
*
|
|
62
|
-
* Uses
|
|
62
|
+
* Uses safeSendInvisibleMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
|
|
63
63
|
* nudge fired during session_before_compact (which is mid-prompt-submission,
|
|
64
64
|
* so the agent can be busy) QUEUES instead of throwing
|
|
65
65
|
* "Agent is already processing. Specify streamingBehavior (steer or followUp)".
|
|
@@ -70,7 +70,7 @@ async function nudgeResume(pi: ExtensionAPI, runtime: MegaRuntime): Promise<void
|
|
|
70
70
|
if (now >= runtime.resumeNudgeUntil) {
|
|
71
71
|
runtime.resumeNudgeUntil = now + 30_000;
|
|
72
72
|
runtime.rt.extensionInitiatedTurn = true; // R13: suppress self-classification
|
|
73
|
-
await
|
|
73
|
+
await safeSendInvisibleMessage(
|
|
74
74
|
pi,
|
|
75
75
|
"[mega-compact] continue from the compacted context above.",
|
|
76
76
|
);
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* Distinct from the poisoned-context /clear advise: the outage advisory is
|
|
10
10
|
* for transient errors where the user's context is fine.
|
|
11
11
|
*
|
|
12
|
-
* PREVENT-PI-003: sends via
|
|
12
|
+
* PREVENT-PI-003: sends via safeSendInvisibleMessage (user-role only).
|
|
13
13
|
* PREVENT-PI-004: local ctx call, no network.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { MegaRuntime } from "../mega-runtime.js";
|
|
17
|
-
import {
|
|
17
|
+
import { safeSendInvisibleMessage } from "./send-safe.js";
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
|
|
20
20
|
/** R11: optional diagnostic detail passed from the classifier. */
|
|
@@ -31,7 +31,7 @@ export interface OutageDetail {
|
|
|
31
31
|
*
|
|
32
32
|
* @param effectiveCategory — must be "transient" for the advisory to fire.
|
|
33
33
|
* @param runtime — the live MegaRuntime (mutated on advisory fire).
|
|
34
|
-
* @param pi — pi ExtensionAPI for
|
|
34
|
+
* @param pi — pi ExtensionAPI for safeSendInvisibleMessage.
|
|
35
35
|
* @param config — providerOutageAdviseThreshold config.
|
|
36
36
|
* @param detail — R11: optional signal + rawText for forensics.
|
|
37
37
|
*/
|
|
@@ -70,8 +70,8 @@ export async function maybeSendProviderOutageAdvisory(
|
|
|
70
70
|
// injecting into the conversation scares users and triggers false-positive
|
|
71
71
|
// re-classification of the resulting turn (2026-07-31 incident).
|
|
72
72
|
if (!config.advisoryChannel) {
|
|
73
|
-
// Legacy path (flag OFF): user-role message
|
|
74
|
-
await
|
|
73
|
+
// Legacy path (flag OFF): invisible user-role message (display:false).
|
|
74
|
+
await safeSendInvisibleMessage(
|
|
75
75
|
pi,
|
|
76
76
|
`[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.`,
|
|
77
77
|
);
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* send-safe.ts — queue-safe
|
|
2
|
+
* send-safe.ts — queue-safe wrappers for pi.sendUserMessage / pi.sendMessage.
|
|
3
3
|
*
|
|
4
4
|
* All extension-initiated user-role nudges (resume after compaction, length-stop
|
|
5
|
-
* continue, error-retry) MUST go through
|
|
6
|
-
* 1.
|
|
5
|
+
* continue, error-retry) MUST go through these wrappers. They:
|
|
6
|
+
* 1. Pass { deliverAs: 'followUp' } so that when the agent is busy (e.g. a
|
|
7
7
|
* resume nudge fired during session_before_compact, which is mid-prompt
|
|
8
8
|
* submission) pi QUEUES the message instead of throwing
|
|
9
9
|
* "Agent is already processing. Specify streamingBehavior (steer or
|
|
10
10
|
* followUp) to queue the message" (pi agent-session.js:830).
|
|
11
|
-
* 2.
|
|
11
|
+
* 2. Await the call and catch-guard it so a failed/queued nudge never throws
|
|
12
12
|
* or produces an unhandled rejection — it must never block the agent loop.
|
|
13
13
|
*
|
|
14
|
+
* safeSendUserMessage renders the text in the conversation UI (visible).
|
|
15
|
+
* safeSendInvisibleMessage delivers via pi.sendMessage with display: false —
|
|
16
|
+
* the agent receives the retry trigger but the user never sees the text.
|
|
17
|
+
*
|
|
14
18
|
* PREVENT-PI-003: user-role sendUserMessage only (no role:'system' injection).
|
|
15
19
|
* PREVENT-PI-004: local pi ctx call, no network.
|
|
16
20
|
*/
|
|
@@ -19,6 +23,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
19
23
|
/**
|
|
20
24
|
* safeSendUserMessage — await + catch-guard + queue-safe wrapper for
|
|
21
25
|
* pi.sendUserMessage. Never throws; never produces an unhandled rejection.
|
|
26
|
+
* The message IS visible in the conversation UI ("Follow-up:").
|
|
22
27
|
*/
|
|
23
28
|
export async function safeSendUserMessage(
|
|
24
29
|
pi: ExtensionAPI,
|
|
@@ -35,3 +40,29 @@ export async function safeSendUserMessage(
|
|
|
35
40
|
/* non-fatal: a failed/queued nudge never blocks the agent loop */
|
|
36
41
|
}
|
|
37
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* safeSendInvisibleMessage — like safeSendUserMessage but uses pi.sendMessage
|
|
46
|
+
* with `display: false` so the retry trigger is delivered to the agent WITHOUT
|
|
47
|
+
* rendering in the conversation UI. Fixes the "Follow-up: [mega-compact] ..."
|
|
48
|
+
* pile-up when the API retries repeatedly — the retry mechanism still fires,
|
|
49
|
+
* the user just doesn't see the queued messages.
|
|
50
|
+
*/
|
|
51
|
+
export async function safeSendInvisibleMessage(
|
|
52
|
+
pi: ExtensionAPI,
|
|
53
|
+
content: string,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
await (
|
|
57
|
+
pi.sendMessage as (
|
|
58
|
+
m: { customType: string; content: string; display: boolean },
|
|
59
|
+
o?: { deliverAs?: "steer" | "followUp" },
|
|
60
|
+
) => Promise<void> | void
|
|
61
|
+
)(
|
|
62
|
+
{ customType: "mega-compact-retry", content, display: false },
|
|
63
|
+
{ deliverAs: "followUp" },
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
/* non-fatal: a failed/queued nudge never blocks the agent loop */
|
|
67
|
+
}
|
|
68
|
+
}
|