Normalise a provider's overflow message so Pi can recover
A scripted provider's own overflow message, normalised in message_end, becomes compact-and-retry; without the handler it is just a failure.
A failed request has three different answers: repeat it (transient), repair
the context and retry once (overflow), or stop. Pi recognises its own
overflow signal; a custom provider’s message may not. The extension below maps
only that provider’s overflow text onto the marker Pi recognises, so recovery
can run. Everything here was observed on 1.0.4 against a scripted provider
in a session with compaction enabled — a real AgentSession, no real model.
Run it
Use Pi 1.0.4 and one of the example repository’s test runners. The file below is the chapter’s canonical extension, reproduced exactly:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
// For a provider whose context-overflow message Pi does not recognise: rewrite
// only that provider's overflow response, and nothing else, so Pi's own
// compact-and-retry recovery can run.
export default function (pi: ExtensionAPI) {
pi.on("message_end", async (event) => {
const msg = event.message;
if (msg.role !== "assistant") return;
if (msg.stopReason !== "error") return;
if (!msg.errorMessage?.includes("model is too long")) return;
return {
message: { ...msg, errorMessage: "context_length_exceeded" },
};
});
}The handler returns { message } — a MessageEndEventResult whose one field
replaces the finalised message — not the message itself. Returning
{ ...msg, errorMessage } directly does not compile and would be silently
ignored, leaving the overflow unnormalised.
The classification boundary
The check is a normalised pattern, not a scan for a word. The handler maps one
provider-specific phrase ("model is too long") at one stop reason
("error") onto the single canonical marker context_length_exceeded that Pi
recognises. A rate-limit message that merely contains the word length is not
that marker and passes through untouched.
Expected outcome
| Message (scripted provider) | Handler loaded? | Observed result |
|---|---|---|
model is too long for this context |
yes | compaction_start with reason: "overflow", then compaction_end with willRetry: true, then a fresh run answers |
model is too long for this context |
no | a plain failure; no compaction attempted |
429 rate limit: request length quota exceeded |
yes | left alone; no compaction |
The failed attempt stays visible in the record: recovery omits it from the
model context with a context_edit entry (append-only, branch-relative) and
appends a compaction entry, instead of deleting it. The retry is a fresh
run, not a resumption.
Mechanism and limitations
Documented: compaction.md pins the single compact-and-retry recovery
attempt for a recognized context-overflow error or early stopReason: "length";
custom-provider.md says to normalize only that provider’s overflow response
in a guarded message_end handler and not to rewrite rate limits or transient
failures as overflow. Observed: all three rows above are asserted by
overflow.test.ts, run on 1.0.4 in-process with a scripted provider and
compaction enabled.
Recovery happens once: if the compaction itself fails or is cancelled, Pi
keeps the omission edits, appends no compaction and schedules no retry. The
handler must keep the message’s role, and the detection is literally the
provider’s own text — a different provider’s overflow wording needs its own
guard. This example does not show a real provider message or a real model
answering; it shows the classification boundary at message_end.
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.