Example

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.

Full source and test .

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.