@mehdisadeghi/pi-retry 0.0.0-stage → 0.11.2

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 CHANGED
@@ -1,3 +1,425 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # 🔄 pi-retry
4
+
5
+ **Automatic retry for every error in [pi](https://github.com/earendil-works/pi-coding-agent)**
6
+
7
+ _400/413, connection errors, credit errors, stream exhaustion — retry them all._
8
+
9
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ---
17
+
18
+ ## Pi 1.0 compatibility (0.10.4)
19
+
20
+ Tested with Pi **1.0.0**. Host-provided Pi packages and TypeBox are peers (`*`), not bundled runtime dependencies; development uses exact Pi 1.0.0 pins and host-compatible TypeBox where needed.
21
+
22
+ Verified the actual `AgentSession.bindExtensions` and `_prepareRetry` lifecycle, including child retry ownership, cancellation and native nested tools. No replacement retry dispatcher is introduced.
23
+
24
+ Run `bun run test:host` for the offline real-host load, native codemode/nested-call, module-identity and reload checks. Set `PI1_HOST_PACKAGE` to an installed Pi package directory to test that host explicitly; add `PI1_HOST_ENTRY=bundle` to check the bundled CLI runtime's constructors.
25
+
26
+ ## Overview
27
+
28
+ This extension automatically detects and retries **all** errors by default, with only a tiny blacklist of known permanent failures (invalid API key, missing model, etc.).
29
+
30
+ | Error Type | Retry Behavior | Use Case |
31
+ |------------|----------------|----------|
32
+ | **Any retryable error** (catch-all) | **Capped exponential retry** | Everything else — provider hiccups, stream exhaustion, credit issues, unknown errors |
33
+ | HTTP 400/413 | **Capped** with exponential backoff, NO compaction | Transient context overflow that might resolve |
34
+ | Credit / payment errors | **Capped** with exponential backoff | "Not Enough Credits", insufficient balance, 402 — top up and the retry loop auto-resumes |
35
+ | **Quota / session-limit / budget exhaustion** | **Not retried** — notify + stop | "You've hit your limit", `insufficient_quota`, "out of budget", suspended accounts |
36
+ | Connection errors | **Capped** with exponential backoff | Network hiccups, connection drops, socket errors, stream exhaustion |
37
+ | Max tokens (`stopReason: "length"`) | **Auto-continue** indefinitely with hidden continuation turns | Model hits output token limit mid-generation |
38
+ | Empty / think-only stop (`stopReason: "stop"` with no text or tool calls) | **Nudge once** with a hidden continuation, then give up | Model ends its turn with no usable output (Anthropic empty responses with end_turn, thinking-only turns) |
39
+
40
+ ---
41
+
42
+ ## The Problem
43
+
44
+ By default, pi has built-in retry for some errors (rate limits, 5xx, overloaded), but:
45
+
46
+ 1. **400/413 errors** are treated as context overflow → triggers compaction but NO retry
47
+ 2. **Connection errors** sometimes get only limited retries before giving up
48
+ 3. **Credit errors** ("Not Enough Credits") are never retried
49
+ 4. **Stream exhaustion** ("Max outbound streams") and other provider-specific errors are never retried
50
+ 5. **Any unknown error** from a new provider is silently ignored
51
+
52
+ ## The Solution
53
+
54
+ This extension provides automatic retry for all errors with configurable exponential backoff and a maximum-delay failure limit (2s → 4s → 8s → ... → 60s by default).
55
+
56
+ **Philosophy: retry EVERYTHING by default.** The only things we skip are a tiny blacklist of known permanent failures (invalid API key, model not found, unsupported model, etc.).
57
+
58
+ **Features:**
59
+ - **Catch-all retry** — Any `stopReason: "error"` is retried, regardless of error message
60
+ - Automatic detection of 400/413, connection, credit, and stream exhaustion errors
61
+ - **Auto-continuation** when the model hits its max output tokens (`stopReason: "length"`) — indefinite, no cap, hidden from the TUI
62
+ - **Retry cutoff** — Keeps retrying until success, abort, or the configured number of failures at the maximum delay
63
+ - **Auto-stop on quota/budget exhaustion** — Session limits, plan quotas, and budget caps ("You've hit your limit", "out of budget", `insufficient_quota`, suspended accounts) are detected and **not** retried, with a notification explaining why
64
+ - Exponential backoff with configurable base delay, cap, multiplier, and maximum-delay failure count
65
+ - **Hidden triggers** — provider-valid custom messages use `display: false`, so retries do not add TUI clutter
66
+ - Manual controls via unified `/retry` command
67
+ - Non-retryable errors are explicitly logged so you know why we didn't retry
68
+
69
+ ---
70
+
71
+ ## Installation
72
+
73
+ ### Option 1: Install via pi package (Recommended)
74
+
75
+ Install directly from GitHub as a pi package:
76
+
77
+ ```bash
78
+ pi install https://github.com/monotykamary/pi-retry
79
+ ```
80
+
81
+ Or add to your `settings.json`:
82
+
83
+ ```json
84
+ {
85
+ "packages": [
86
+ "https://github.com/monotykamary/pi-retry"
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ### Option 2: Global Installation
92
+
93
+ Copy the extension to pi's global extensions directory:
94
+
95
+ ```bash
96
+ cp retry.ts ~/.pi/agent/extensions/
97
+ ```
98
+
99
+ ### Option 3: Project-Local Installation
100
+
101
+ Copy to your project's `.pi/extensions/` directory:
102
+
103
+ ```bash
104
+ mkdir -p .pi/extensions
105
+ cp retry.ts .pi/extensions/
106
+ ```
107
+
108
+ ### Option 4: Quick Test
109
+
110
+ ```bash
111
+ pi -e ./retry.ts
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Usage
117
+
118
+ Once loaded, the extension **automatically** detects and retries all errors.
119
+
120
+ ### Manual Controls
121
+
122
+ | Command | Description |
123
+ |---------|-------------|
124
+ | `/retry` | Manually trigger immediate retry (auto-detects: 400/413, credit, connection, max_tokens, or any other error) |
125
+ | `/retry status` | Show current retry diagnostics for all error types + continuation state |
126
+ | `/retry reset` | Reset all retry counters and state |
127
+
128
+ ---
129
+
130
+ ## Configuration
131
+
132
+ The extension reads a `piRetry` object from Pi's settings files:
133
+
134
+ - `~/.pi/agent/settings.json` applies globally.
135
+ - `.pi/settings.json` overrides matching global values for the current project.
136
+
137
+ ```json
138
+ {
139
+ "piRetry": {
140
+ "baseDelayMs": 10000,
141
+ "maxDelayMs": 3600000,
142
+ "multiplier": 2,
143
+ "maxRetriesAtMaxDelay": 3
144
+ }
145
+ }
146
+ ```
147
+
148
+ The example above waits 10 seconds before the first retry, doubles each delay, caps the delay at one hour, and stops after three failed retries at that cap. Supported values are:
149
+
150
+ | Setting | Default | Description |
151
+ |---------|---------|-------------|
152
+ | `baseDelayMs` | `2000` | Delay before the first retry, in milliseconds |
153
+ | `maxDelayMs` | `60000` | Maximum delay between retries, in milliseconds |
154
+ | `multiplier` | `2` | Exponential backoff multiplier; must be at least `1` |
155
+ | `maxRetriesAtMaxDelay` | `3` | Failed ordinary retries allowed after the delay reaches `maxDelayMs` |
156
+ | `messageFreeRetries` | `false` | Retry errors without a hidden retry message (see below) |
157
+
158
+ `piRetry` is separate from Pi's built-in `retry` object so the two retry policies do not share ambiguous settings. The extension disables Pi's native retry scheduler while it is loaded, while preserving Pi's compaction handling, so only one retry loop owns the backoff schedule.
159
+
160
+ Settings are read when the extension starts. Restart pi or use `/reload` after editing them.
161
+
162
+ ### Message-free error retries (opt-in)
163
+
164
+ By default, each error retry queues a hidden `Retry the previous request.`
165
+ message, which stays in the model context and the session file. With
166
+ `"messageFreeRetries": true`, an automatic error retry instead waits out the
167
+ backoff inside the original prompt, then uses Pi's `agent_before_settle`
168
+ boundary to omit the failed response with a `context_edit` entry and continue
169
+ the unchanged context. The provider receives the same request again, and the
170
+ omission persists when the session is resumed. The failed response stays in
171
+ the raw session history.
172
+
173
+ - Max-token continuations, empty-stop nudges, and manual `/retry` still use
174
+ hidden messages, because they need a new user turn to continue.
175
+ - Escape, new input, the backoff schedule, and the retry cap work as before.
176
+ The prompt stays busy during the backoff, so input typed then is steered
177
+ into the run and cancels the pending retry.
178
+ - Requires Pi with the `agent_before_settle` boundary (0.87.0 or later). On
179
+ older Pi the extension warns once and keeps hidden retry messages.
180
+
181
+ ---
182
+
183
+ ## How It Works
184
+
185
+ 1. **Listen to `agent_end` event** — Fires after each agent turn completes
186
+ 2. **Check for any error** — Examine the last assistant message for `stopReason === "error"`
187
+ 3. **Blacklist check** — Skip known permanent failures (invalid API key, model not found, quota/session-limit/budget exhaustion, suspended accounts, etc.)
188
+ 4. **Categorize for messaging** — Classify into 400/413, credit, connection, or other for nice UI notifications
189
+ 5. **Retry or continue with hidden turns** — Wait with exponential backoff, then trigger a provider-valid custom user turn via `pi.sendMessage()` with `display: false` and `triggerTurn: true` (with `messageFreeRetries`, error retries omit the failed response and continue instead)
190
+ 6. **Valid provider context** — Hidden retry and continuation messages remain in context so providers never receive a trailing assistant message
191
+ 7. **Indefinite continuation** — Max_tokens auto-continues are uncapped; repeated `length` stops keep producing continuation turns until the model terminates normally
192
+ 8. **Empty-stop recovery** — A `stop` turn with no usable output (only thinking, or nothing at all) gets exactly one hidden "nudge" continuation, then gives up. Matches Anthropic's documented "empty responses with `end_turn`" remedy (continuation prompt in a new user message) — retrying an empty response in place doesn't help because the model has already decided it's done.
193
+ 8. **Lifecycle exposure** — Emits `pi-retry:started`, `pi-retry:completed`, and `pi-retry:cancelled` on Pi's shared extension event bus with a matching `retryId`, allowing status integrations to suppress intermediate completion signals
194
+
195
+ The pi's built-in `transform-messages` already strips aborted/errored assistant messages from the LLM context, so the model never sees the failed attempts.
196
+
197
+ ---
198
+
199
+ ## Detected Error Patterns
200
+
201
+ ### Catch-All (Any Error)
202
+ - **Any** assistant message with `stopReason === "error"` is retried by default
203
+ - Unknown provider errors, stream errors, unexpected failures — all handled automatically
204
+ - Only skipped if it matches a known permanent failure (invalid API key, missing model, etc.)
205
+
206
+ ### Non-Retryable (Permanent Failures)
207
+ These are explicitly **not** retried:
208
+ - Invalid API key / invalid authentication
209
+ - API key not found / missing / revoked
210
+ - Model not found / unknown model / no such model / model does not exist
211
+ - Unsupported model
212
+
213
+ ### Non-Retryable (Quota / Session Limit / Budget)
214
+ Exhausted quotas, session limits, and budgets are auto-detected and stop the retry loop (with an explanatory notification), because retrying is pointless until you act or the reset window passes:
215
+ - **Usage / session limits with reset windows** — "You've hit your limit · resets …" and "5-hour limit reached" (Claude Code), "You've hit your usage limit" / "You've exceeded your usage limit" (Codex), "You have hit your ChatGPT usage limit (plus plan)" (ChatGPT subscription caps via the Codex backend, also `usage_limit_reached`)
216
+ - **Plan / billing quotas** — OpenAI `insufficient_quota`, "You exceeded your current quota, please check your plan and billing details" (OpenAI, Gemini — reached only after pi's built-in 429 retry gives up)
217
+ - **Google subscription caps** — "You have exhausted your capacity on this model. Your quota will reset after …" (Gemini Code Assist), "You have reached the quota limit for …" / "You can resume using this model at …" (Antigravity)
218
+ - **Hard allotments** — OpenRouter `free-models-per-day`, Alibaba Coding Plan window quotas "hour/week/month allocated quota exceeded" and free-quota exhaustion "free allocated quota exceeded" (the bare "Allocated quota exceeded" is TPM rate limiting and stays retryable), GitHub Copilot "premium request allowance", z.ai GLM Coding Plan "Usage limit reached for 5 hour" / "no resource package"
219
+ - **Budget exhaustion** — "out of budget", "Budget has been exceeded" (LiteLLM-style proxies), max/spending/monthly limits
220
+ - **Suspended accounts** — "Your account … is suspended" (Kimi `exceeded_current_quota_error`)
221
+
222
+ Deliberate distinction: plain pay-as-you-go **balance** errors stay retryable — DeepSeek 402 "Insufficient Balance", OpenRouter 402 "Insufficient credits", Kimi "exceeded your current token quota". A mid-session top-up lets the retry loop auto-resume, whereas session limits and budgets do not self-resolve for hours.
223
+
224
+ ### Max Tokens (stopReason: "length")
225
+ - The model hit its `max_tokens` / output token limit
226
+ - The model's response was truncated mid-generation
227
+ - Auto-continuation sends a provider-valid custom message hidden from the TUI
228
+
229
+ ### 400/413 Errors
230
+ - HTTP 400 Bad Request
231
+ - HTTP 413 Payload Too Large
232
+ - "bad request" messages
233
+ - "payload too large" messages
234
+
235
+ ### Credit / Payment Errors
236
+ - "Not Enough Credits"
237
+ - "insufficient credits"
238
+ - "insufficient balance"
239
+ - "out of credits"
240
+ - "Payment Required"
241
+ - HTTP 402 status code
242
+
243
+ ### Connection Errors
244
+ - Connection / network errors
245
+ - Fetch failures
246
+ - Socket hang up / socket errors
247
+ - `ECONNRESET`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`
248
+ - DNS lookup failures
249
+ - "Request ended without sending any chunks"
250
+ - Upstream connect errors
251
+ - TLS handshake errors
252
+ - Timeouts awaiting response
253
+ - Stream exhaustion ("Max outbound streams is 100, 100 open")
254
+ - Stream limit errors
255
+
256
+ ---
257
+
258
+ ## Development
259
+
260
+ ### Running Tests
261
+
262
+ ```bash
263
+ # Run all tests
264
+ npm test
265
+
266
+ # Run tests in watch mode
267
+ npm run test:watch
268
+
269
+ # Run tests with coverage
270
+ npm run test:coverage
271
+
272
+ # Type check
273
+ npm run typecheck
274
+
275
+ # Dead code detection
276
+ npm run lint:dead
277
+ ```
278
+
279
+ ### Project Structure
280
+
281
+ ```
282
+ .
283
+ ├── retry.ts # Main unified extension
284
+ ├── src/ # Shared utilities (testable, DRY)
285
+ │ ├── config.ts # Settings-backed retry configuration
286
+ │ ├── error-patterns.ts # Error pattern matching, custom types, hasMaxTokensStop
287
+ │ ├── retry-logic.ts # Retry utilities (calculateDelay, RetryState, ContinuationState, etc.)
288
+ │ └── index.ts # Barrel exports
289
+ ├── __tests__/ # Unit tests
290
+ │ └── unit/
291
+ │ ├── config.test.ts
292
+ │ ├── error-patterns.test.ts
293
+ │ └── retry-logic.test.ts
294
+ ├── vitest.config.ts # Test configuration
295
+ └── knip.json # Dead code detection config
296
+ ```
297
+
298
+ ### Code Quality
299
+
300
+ ```bash
301
+ # Run all quality checks
302
+ npm test # 224 tests
303
+ npm run typecheck # TypeScript type checking
304
+ npm run lint:dead # Dead code detection with knip
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Troubleshooting
310
+
311
+ ### Extension not working?
312
+
313
+ Check that it's loaded in the startup header:
314
+ ```
315
+ Loaded extensions: retry.ts
316
+ ```
317
+
318
+ ### Retry not triggering?
319
+
320
+ Use the status command to diagnose:
321
+ ```
322
+ /retry status
323
+ ```
324
+
325
+ ### Want to see what's happening?
326
+
327
+ The extensions send notifications on retry attempts. Look at the footer status line for retry status updates. Non-retryable errors are logged as errors so you know why we stopped.
328
+
329
+ ### Too many retries?
330
+
331
+ Use `/retry reset` to clear the counters, or press `Ctrl+C` to abort the session.
332
+
333
+ ---
334
+
335
+ ## Comparison with @georgebashi/pi-retry
336
+
337
+ The npm package `@georgebashi/pi-retry` handles "aborted" streaming errors but explicitly excludes "connection error" (assuming pi's built-in retry handles it). This extension:
338
+
339
+ 1. **Handles ALL errors** via a catch-all — no more playing whack-a-mole with new error patterns
340
+ 2. **Handles connection errors** that pi might not retry sufficiently
341
+ 3. **Handles 400/413 errors** without compaction
342
+ 4. **Handles credit errors** and stream exhaustion
343
+
344
+ They can work together for maximum coverage:
345
+
346
+ ```bash
347
+ pi install npm:@georgebashi/pi-retry
348
+ # Plus install this extension
349
+ ```
350
+
351
+ ---
352
+
353
+ ## Limitations
354
+
355
+ - Pi's native retry scheduler is disabled while this extension is loaded so native and extension retries cannot interleave; Pi's compaction check still runs normally
356
+ - Error messages remain in the session history (but are invisible to the LLM)
357
+ - May hit the same error repeatedly if the issue is persistent (use `Ctrl+C` to abort)
358
+ - **Warning**: Retrying 400/413 without reducing context may fail repeatedly if the payload is genuinely too large
359
+ - Non-retryable errors (invalid API key, missing model, quota/session-limit/budget exhaustion) are logged but not retried — you'll need to fix the underlying issue, then use `/retry`
360
+
361
+ ---
362
+
363
+ ## Related
364
+
365
+ - [Pi Coding Agent Extensions Docs](https://github.com/badlogic/pi/tree/main/packages/coding-agent/docs/extensions.md)
366
+ - [@georgebashi/pi-retry](https://github.com/georgebashi/pi-retry) — Handles "aborted" streaming errors
367
+ - [Issue #252: Connection error with no retry](https://github.com/badlogic/pi-mono/issues/252)
368
+
369
+ ## License
370
+
371
+ MIT
372
+
373
+ ## Subagent retry policies
374
+
375
+ Sessions that should retry with a different policy can be selected by matching
376
+ their effective system prompt. This is user configuration: the extension ships
377
+ no built-in marker or identity check for any specific subagent tool.
378
+
379
+ ```json
380
+ {
381
+ "piRetry": {
382
+ "subagents": {
383
+ "enabled": true,
384
+ "match": {
385
+ "systemPromptRegex": [
386
+ {
387
+ "pattern": "^<active_agent name=\"[^\"\\r\\n]+\"/>$",
388
+ "flags": "m"
389
+ }
390
+ ]
391
+ },
392
+ "baseDelayMs": 1000,
393
+ "maxDelayMs": 10000,
394
+ "maxRetriesAtMaxDelay": 2
395
+ }
396
+ }
397
+ }
398
+ ```
399
+
400
+ Semantics:
401
+
402
+ - Sessions whose effective system prompt matches any listed rule use the
403
+ inline child policy; rules combine with OR.
404
+ - Omitted child fields inherit from the effective top-level policy, field by
405
+ field. Project matcher lists replace global ones.
406
+ - A matching rule with `enabled: false` leaves pi's native retry scheduler
407
+ untouched; a missing, empty, malformed, or nonmatching configuration keeps
408
+ the ordinary `pi-retry` takeover.
409
+ - Unmatched sessions in print or JSON mode (`pi -p`, `pi --mode json`, and SDK
410
+ hosts that bind extensions without a mode, such as pi-subagents children)
411
+ retry inline with the top-level policy, inside the original prompt. These
412
+ hosts read the result as soon as `prompt()` settles, so a detached retry
413
+ would arrive too late. A matcher is only needed for a different child policy
414
+ or for `enabled: false`.
415
+ - Patterns are compiled when settings resolve. Invalid syntax, unsupported or
416
+ duplicate flags, and malformed groups warn and become no-match instead of
417
+ matching everything.
418
+ - The extension must also be loaded in the target session (for pi-subagents
419
+ that means listing it in the child's `subagentOnlyExtensions`), not only in
420
+ the parent.
421
+
422
+ The example pattern above matches the `<active_agent name="…"/>` line that
423
+ `pi-subagents` prefixes to native child system prompts; substitute your own
424
+ marker if you select child sessions differently. Keep the `m` flag: some
425
+ children have a `<sub_agent_context>` block before that line.
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@mehdisadeghi/pi-retry",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.11.2",
4
+ "description": "Extension suite for pi coding agent that handles 400/413 errors and connection errors with automatic retry",
5
+ "type": "module",
6
+ "author": "Tom X Nguyen",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/monotykamary/pi-retry.git"
11
+ },
12
+ "homepage": "https://github.com/monotykamary/pi-retry#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/monotykamary/pi-retry/issues"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi",
19
+ "pi-coding-agent",
20
+ "extension",
21
+ "retry",
22
+ "error-handling",
23
+ "http-400",
24
+ "http-413",
25
+ "connection-error",
26
+ "network-error",
27
+ "infinite-retry",
28
+ "cli"
29
+ ],
30
+ "files": [
31
+ "*.ts",
32
+ "src/",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "test:host": "node scripts/pi1-host.mjs",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "test:coverage": "vitest run --coverage",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint:dead": "knip --no-gitignore"
42
+ },
43
+ "peerDependencies": {
44
+ "@earendil-works/pi-agent-core": "*",
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*",
47
+ "@earendil-works/pi-ai": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-ai": "1.3.0",
51
+ "@earendil-works/pi-agent-core": "1.3.0",
52
+ "@earendil-works/pi-coding-agent": "1.3.0",
53
+ "@earendil-works/pi-tui": "1.3.0",
54
+ "@types/node": "25.9.1",
55
+ "@vitest/coverage-v8": "4.1.7",
56
+ "knip": "6.14.1",
57
+ "typescript": "6.0.3",
58
+ "vitest": "4.1.7"
59
+ },
60
+ "pi": {
61
+ "extensions": [
62
+ "./retry.ts"
63
+ ]
64
+ },
65
+ "overrides": {
66
+ "brace-expansion": "5.0.12"
67
+ }
68
+ }