pi-pattern-retry
v0.1.0
Published
Pi extension: keep agent sessions alive across provider rate-limits, quota exhaustion, and transient auth failures by re-injecting continuation messages on an escalating retry schedule.
Maintainers
Readme
pi-pattern-retry
Keep your Pi Agent session alive across provider rate-limits, quota exhaustion, and transient auth failures. pi-pattern-retry watches provider responses and agent errors, matches them against configurable patterns (regex / substring), and re-injects a continuation message at escalating intervals — 30s → 1m → 5m → 15m → 30m → 1h → 4h → 5h → 12h → 24h by default — so the agent picks up where it left off.
Designed alongside todo-enforcer: same retry-scheduler idea, separate package, ships independently.
Install
pi install npm:pi-pattern-retryThen restart your Pi session. The plugin ships with sensible defaults — no config file is required to get started.
What it does
Two trigger sources feed one matcher:
| Event | What it inspects |
|---------------------------|------------------------------------------------------------------|
| after_provider_response | HTTP status — catches 401 / 403 / 429 / 5xx |
| agent_end | last assistant message's errorMessage — catches transport text |
A match schedules one timer per (sessionId, patternName). When the timer fires:
- If the conversation has progressed (branch grew, excluding our own injections), reset attempt counter and stop.
- Otherwise inject
pi.sendUserMessage(<your template>)so the agent retries. - Advance to the next delay in the schedule. When the schedule is exhausted, the pattern stays inert until
/pattern-retry-reset.
Duplicate triggers (e.g. both events firing for the same failure) are debounced — the already-live timer wins.
Default patterns
Shipped with three patterns pre-seeded. Override or replace via config file.
| Name | Trigger | Schedule (seconds) |
|------------------------|----------------------|------------------------------------------------------------------------|
| llm-429-quota | HTTP 429 | 30, 60, 300, 900, 1800, 3600, 14400, 18000, 43200, 86400 (10 steps) |
| llm-401-403-auth | HTTP 401 / 403 | 60, 300, 1800 (3 steps) |
| litellm-weekly-limit | errorMessage contains "Weekly/Monthly Limit Exhausted" | 3600, 14400, 43200, 86400 |
Configuration
Global file: ~/.pattern-retry.json. Project override: <cwd>/.pattern-retry.json. Project keys override global; arrays replace, they do not merge.
{
"enabled": true,
"patterns": [
{
"name": "llm-429-quota",
"match": { "source": "providerStatus", "kind": "regex", "value": "^429$" },
"scheduleSec": [30, 60, 300, 900, 1800, 3600, 14400, 18000, 43200, 86400],
"message": "Provider returned {{status}} (attempt {{attempt}}/{{maxAttempts}}). Retrying after {{lastDelaySec}}s.",
"stopOnProgress": true,
"notify": ["webhook", "dashboard"]
},
{
"name": "litellm-weekly-limit",
"match": {
"source": "errorMessage",
"kind": "contains",
"value": "Weekly/Monthly Limit Exhausted"
},
"scheduleSec": [3600, 14400, 43200, 86400],
"message": "Hit weekly LLM quota. Resuming attempt {{attempt}}."
}
],
"notifyTargets": {
"webhook": { "enabled": false, "url": "", "headers": {}, "timeoutMs": 10000 },
"dashboard": { "enabled": false, "endpoint": "" }
}
}Match semantics
kind: "contains"— case-sensitive substring match.kind: "regex"— compiled withnew RegExp(value); no implicit flags. For case-insensitive, encode flags in the pattern itself.source: "providerStatus"— matched against the decimal status string (e.g."429").source: "errorMessage"— matched against the assistant message'serrorMessagestring. If absent/empty, the pattern does not match.
A malformed regex degrades to a logged warning — other patterns continue to work.
Template variables
| Variable | Value |
|--------------------|----------------------------------------------------------------|
| {{status}} | HTTP status from providerStatus, or 0 |
| {{statusText}} | Human label (e.g. "Too Many Requests") |
| {{reason}} | Match-source snippet |
| {{patternName}} | The matched pattern's name |
| {{attempt}} | 1-indexed current attempt |
| {{maxAttempts}} | scheduleSec.length |
| {{lastDelaySec}} | Delay that elapsed before this fire |
| {{nextDelaySec}} | Next delay if any, else 0 |
Missing variables interpolate to empty string.
Commands
/pattern-retry-status— show each pattern'sattempt/max,exhausted, andscheduledflags./pattern-retry-reset [patternName|all]— cancel timer(s) and reset attempt counter./pattern-retry-toggle— flip the globalenabledflag./pattern-retry-log [N]— tail the lastNlines (default 50, max 500) of the central log file straight into the session.
Troubleshooting / Logs
Every meaningful decision is appended to a single central log file:
~/.pi/logs/extensions/pattern-retry.logThis is the shared convention across all pi extensions — written via the rotated, size-capped (25 MB, 80% trim) createPluginLogger. Each line is:
<iso-timestamp> [pattern-retry] <LEVEL> <event> <json-details>Events you can grep for:
| Event | Means |
|--------------------------------|------------------------------------------------------------------------|
| session-start | Session bound to pattern-retry; sessionId + cwd recorded |
| config-loaded / config-load failed | Result of merging ~/.pattern-retry.json + project override |
| trigger:matched | A provider/error trigger arrived; lists matched pattern names |
| trigger:skipped-disabled / trigger:skipped-no-session | Trigger dropped — explains why |
| schedule:armed | Timer set; attempt, delaySec, trigger metadata |
| schedule:skip-idempotent | Duplicate trigger dropped because a live timer already exists |
| schedule:skip-exhausted / schedule:exhausted | All attempts used up |
| timer:elapsed | Scheduled delay elapsed; about to fire |
| fire:injected | Continuation message sent via pi.sendUserMessage |
| fire:reset-on-progress | Agent advanced before timer fired → attempt counter reset, no send |
| cancel:pattern / cancel:session | /pattern-retry-reset or session shutdown |
| command:toggle / command:reset / command:reset-all | Audit trail for user commands |
| notify:<event>:<target> | A configured notify target fired (V1: log-only) |
When something looks wrong:
tail -f ~/.pi/logs/extensions/pattern-retry.log
# or from inside a session:
/pattern-retry-log 200The file is the source of truth — every fire, skip, reset, and exhaustion is there with the sessionId and pattern name attached, so you can reconstruct exactly what happened and why.
Notify hooks (V1: no-op)
Each pattern may carry notify?: ("webhook" | "dashboard")[]. V1 logs one line per enabled target — real HTTP/WebSocket senders ship in V2 (see flow/plans/pattern-retry-future.md upstream).
The notifyHooks() signature is the public contract — V2 swaps the implementation without changing callers.
How it differs from todo-enforcer
| Plugin | Trigger | Use case |
|---------------------|----------------------------------|-------------------------------------------------|
| todo-enforcer | Idle agent with pending todos | Nudge the agent to keep working on its list |
| pi-pattern-retry | Provider error / quota signal | Resume after an external rate-limit or outage |
They share the retry-scheduler pattern (copy-adapted), not code — install one without the other.
License
MIT
