Reviewed related GitHub issue and PR.
UnderstandRead retry logic code in detail.
UnderstandImplemented fix to reset retry attempts properly.
EditUpdated changelog documentation.
EditRan checks and closed issue after successful push.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/1019 also check the pr
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #1019 and also check the associated PR. Let me start by fetching the issue details.
>gh issue view 1019 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"When hitting rate limits during tool-use turns, the retry counter accumulates across separate LLM calls instead of resetting after each successful response.\n\n**Steps to reproduce:**\n1. Use a rate-limited provider (e.g., Baseten)\n2. Send a prompt that triggers tool use with multiple LLM calls\n3. Each LLM call hits rate limit, retries once, succeeds\n4. After 3 such cycles, see '3/3 max retries exceeded' and fail\n\n**Expected:** Each successful LLM response should reset the retry counter.\n\n**Actual:** Counter accumulates: tool call 1 retry + tool call 2 retry + tool call 3 retry = 3/3 exceeded.\n\n**Fix:** Reset `_retryAttempt` immediately when a successful (non-error) assistant message arrives in the message_end handler, not just at agent_end.","comments":[],"labels":[],"state":"OPEN","title":"Auto-retry counter accumulates across LLM calls within a turn"}>gh pr list --repo badlogic/pi-mono --search "1019" --json number,title,state,headRefName[]The issue is about the auto-retry counter accumulating across LLM calls within a turn. Let me search for any PR that might be related to this issue.
Let me search for PRs more broadly:
>gh pr list --repo badlogic/pi-mono --state all --json number,title,state,headRefName | head -30[{"headRefName":"fix/deepseek-developer-role","number":1047,"state":"CLOSED","title":"fix(ai): add DeepSeek to non-standard providers"},{"headRefName":"fix/settings-external-edit-preservation","number":1046,"state":"OPEN","title":"fix: preserve external settings.json edits on reload"},{"headRefName":"quiet-header","number":1039,"state":"OPEN","title":"create headerContainer for custom header"},{"headRefName":"fix/backslash-input-buffering","number":1037,"state":"OPEN","title":"fix(tui): remove backslash input buffering"},{"headRefName":"upgrade-github-actions-node24-general","number":1030,"state":"CLOSED","title":"Upgrade GitHub Actions to latest versions"},{"headRefName":"upgrade-github-actions-node24","number":1029,"state":"CLOSED","title":"Upgrade GitHub Actions for Node 24 compatibility"},{"headRefName":"fix/discord-stacktrace","number":1027,"state":"CLOSED","title":"surface error stacks stacks in output errorMessage"},{"headRefName":"fix/gemini-tool-conversion","number":1025,"state":"CLOSED","title":"fix(ai): convert anyOf/const schemas to enum for Google providers"},{"headRefName":"fix/autocomplete-improvements","number":1024,"state":"OPEN","title":"A couple of autocomplete improvements"},{"headRefName":"main","number":1023,"state":"CLOSED","title":"feat(ai): add support for volcengine provider"},{"headRefName":"feat/openai-retry","number":1018,"state":"CLOSED","title":"fix(coding-agent): reset retry counter after each successful LLM response"},{"headRefName":"feat/oracle-launchpad-widget","number":1011,"state":"CLOSED","title":"feat(widget): Add Oracle Launchpad (Lunar-Gated Controls)"},{"headRefName":"fix/web-ui-typo-setting","number":1010,"state":"CLOSED","title":"fix(web-ui): fix typo in comment"},{"headRefName":"fix/ai-typo-status","number":1009,"state":"CLOSED","title":"fix(ai): fix typo in test comment"},{"headRefName":"fix/web-ui-export-debug-components","number":1008,"state":"CLOSED","title":"fix(web-ui): export AbortedMessage and ToolMessageDebugView"},{"headRefName":"fix/web-ui-export-provider-key-input","number":1007,"state":"CLOSED","title":"fix(web-ui): export ProviderKeyInput component"},{"headRefName":"fix/web-ui-export-custom-provider-card","number":1006,"state":"CLOSED","title":"fix(web-ui): export CustomProviderCard component"},{"headRefName":"feat/tactical-launchpad","number":1005,"state":"CLOSED","title":"feat(ui): Add Tactical Launchpad (Macro Control Grid)"},{"headRefName":"main","number":1004,"state":"MERGED","title":"perf(tui): optimized extractCursorPosition to scan lines in reverse order"},{"headRefName":"feat/crt-overlay","number":1003,"state":"CLOSED","title":"feat(web-ui): Add Proximity Radar widget"},{"headRefName":"feat/crt-overlay","number":1002,"state":"CLOSED","title":"feat(ui): Add Retro CRT/Phosphor Effect Layer"},{"headRefName":"feature/binary-docs","number":999,"state":"OPEN","title":"fix(build): add HTML templates and documentation to binary builds"},{"headRefName":"openai-completions-tools-fix","number":998,"state":"MERGED","title":"OpenAI completions toolChoice fix"},{"headRefName":"fix-table-rendering","number":997,"state":"MERGED","title":"fix(tui): improve table rendering with row dividers and min width"},{"headRefName":"feat/rpc-get-commands","number":995,"state":"MERGED","title":"Add `get_commands` RPC for headless clients"},{"headRefName":"feat/autocomplete-auto-apply-single","number":993,"state":"MERGED","title":"feat(tui): auto-apply single suggestion in force file autocomplete"},{"headRefName":"patch-1","number":991,"state":"CLOSED","title":"Update README.md"},{"headRefName":"fix/openrouter-reasoning-details","number":990,"state":"CLOSED","title":"fix: preserve all reasoning_details types for OpenRouter models"},{"headRefName":"fix/openrouter-reasoning-details","number":988,"state":"CLOSED","title":"fix(ai): preserve all reasoning_details for OpenRouter models"},{"headRefName":"fix/kimi","number":987,"state":"OPEN","title":"fix(ai): include reasoning_content for OpenRouter Kimi K2.5 tool calls"}]>gh pr list --repo badlogic/pi-mono --state open --json number,title,state,headRefName,body[{"body":"Editing `settings.json` directly while Pi is running would get your changes silently reverted on reload. This happens because Pi calls `setLastChangelogVersion()` on startup which triggers `save()`, and the old logic would re-read the file but then merge the entire in-memory state on top, so your edit to `packages` gets overwritten by whatever value was loaded when Pi first started.\r\n\r\nThere was a prior fix for this (#527) that added the re-read + merge logic, but `deepMergeSettings()` uses \"in-memory wins\" for arrays so it preserved *new* fields added to the file, but not *modifications* to existing fields like `packages`.\r\n\r\nThis fix tracks which fields actually changed via setters and only writes those back. Added a `modifiedFields` Set that each setter marks before calling `save()`, and tests that fail on main / pass with this fix.\r\n\r\nAlso added test cases that reproduce the bug.","headRefName":"fix/settings-external-edit-preservation","number":1046,"state":"OPEN","title":"fix: preserve external settings.json edits on reload"},{"body":"I recently noticed that the custom headers introduced in #500 do not play nice with the `quietStartup` setting. Specifically, the custom header remains stuck to the text input, even after exchanging multiple messages (see below)\r\n<img width=\"422\" height=\"516\" alt=\"Screenshot 2026-01-28 at 15 32 54\" src=\"https://github.com/user-attachments/assets/cb13803a-72b8-4b30-91d6-c2de447dad61\" />\r\n\r\nFixed this by wrapping this custom header in a dedicated `headerContainer` (see [pi session fixing it](https://buildwithpi.ai/session/#e54f828f680c23f9371a231e49505db7))","headRefName":"quiet-header","number":1039,"state":"OPEN","title":"create headerContainer for custom header"},{"body":"### Problem\r\n\r\nPreviously, pressing `\\` would buffer the character and wait for the next key press. If followed by <kbd>Enter</kbd>, it would insert a newline, <kbd>Shift+Enter</kbd> equivalent. If followed by any other key, both characters would be inserted. This caused a noticeable input delay when typing backslashes.\r\n\r\n### Cause\r\n\r\nThe feature was added in commit 178a3a56 \"fix(tui): handle split Shift+Enter in VS Code\".\r\n\r\nI believe the purpose was supporting an old Claude Code VS Code keybinding. The `/terminal-setup` command used to add:\r\n\r\n```json\r\n{\r\n \"key\": \"shift+enter\",\r\n \"command\": \"workbench.action.terminal.sendSequence\",\r\n \"args\": { \"text\": \"\\\\\\r\\n\" }\r\n}\r\n```\r\n\r\nThis sent a literal backslash + CR + LF when pressing <kbd>Shift+Enter</kbd>. The buffering detected this pattern and converted it to a newline. Newer Claude Code versions changed this to `\"\\^[\\r\"` <kbd>Alt+Enter</kbd>, and Pi's docs now recommend the Kitty sequence `\"\\^[[13;2u\"`.\r\n\r\n### Solution\r\n\r\nI decided to keep this behavior because I believe a few people have developed muscle memory for the `\\+Enter` combination. Instead of buffering, let `\\` be inserted immediately. On <kbd>Enter</kbd>, check if the character before the cursor is `\\`. If so, delete it and insert a newline instead of submitting.\r\n\r\nI removed the backslash handling entirely from the Input component. These are single-line inputs (search boxes, rename fields); there's no need to insert new lines there.\r\n\r\n\r\nhttps://github.com/user-attachments/assets/41707307-db5e-4b7b-b6de-074a1bff5cf4\r\n\r\n","headRefName":"fix/backslash-input-buffering","number":1037,"state":"OPEN","title":"fix(tui): remove backslash input buffering"},{"body":"## Keep file suggestions open when typing in the Tab-triggered mode\r\n\r\n### Problem\r\n\r\nWhen pressing <kbd>Tab</kbd> on an empty prompt or after a space, file suggestions appear as expected. However, typing a character would behave inconsistently:\r\n\r\n- Typing `.` would narrow the suggestions to files starting with `.`\r\n- Typing a letter like `a` would dismiss the suggestions entirely\r\n\r\n### Solution\r\n\r\nTrack how autocomplete was triggered:\r\n\r\n- If it was triggered by <kbd>Tab</kbd>, then use `getForceFileSuggestions` when updating, which always extracts the prefix and filters files regardless of whether it looks like a path.\r\n- If it was triggered triggered by typing `/` or `@`, then use `getSuggestions` as before.\r\n\r\nHere's a screencap of the problem and the solution:\r\n\r\nhttps://github.com/user-attachments/assets/ba18a4d9-428f-4c35-8e2b-fd365718e976\r\n\r\n## Hide autocomplete when backspacing a slash command\r\n\r\n### Problem\r\n\r\nWhen typing `/` in an empty prompt, slash command suggestions appear as expected. However, pressing <kbd>Backspace</kbd> to delete the `/` causes file suggestions to appear instead of hiding all suggestions.\r\n\r\n### Cause\r\n\r\nIn `CombinedAutocompleteProvider.extractPathPrefix()`, when the text before the cursor is completely empty (`\"\"`), the function returned an empty string which triggered file suggestions for the current directory. The condition was:\r\n\r\n```typescript\r\nif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\r\n return pathPrefix;\r\n}\r\n```\r\n\r\nThis caused `getSuggestions()` to return file completions for an empty prefix after the slash was deleted.\r\n\r\n### Solution\r\n\r\nChanged `extractPathPrefix()` to only return an empty string for file completion when the text ends with a space, e.g. `\"hello \"`, but not when the text is completely empty. Empty text should not trigger automatic file suggestions – that behavior is reserved for explicit <kbd>Tab</kbd> completion.\r\n\r\nHere's a screencap of the problem and the solution:\r\n\r\nhttps://github.com/user-attachments/assets/96bbcb00-20fe-4a09-ade7-047af2efcd0e\r\n\r\n\r\n","headRefName":"fix/autocomplete-improvements","number":1024,"state":"OPEN","title":"A couple of autocomplete improvements"},{"body":"The binary archive was previously missing docs/ and export-html/, so pi couldn't answer questions about itself or generate HTML exports.","headRefName":"feature/binary-docs","number":999,"state":"OPEN","title":"fix(build): add HTML templates and documentation to binary builds"},{"body":"## Problem\r\n\r\n```\r\nError: 400 Provider returned error\r\n {\"error\":{\"message\":\"thinking is enabled but reasoning_content is missing in assistant tool call message at index 2\",\"type\":\"invalid_request_error\"}}\r\n```\r\n\r\n## Summary\r\nOpenRouter's Kimi K2.5 rejects assistant tool-call messages when thinking is enabled but `reasoning_content` is missing. This patch ensures `reasoning_content` is always present (and correctly mapped) for OpenRouter tool-call messages.\r\n\r\n## Root Cause\r\nThinking blocks from prior assistant messages were being forwarded using the `reasoning` field, while OpenRouter/Kimi expects `reasoning_content` specifically.\r\n\r\n## Changes\r\n- Treat reasoning as enabled when a thinking level is requested.\r\n- For OpenRouter, map thinking signatures `reasoning`, `reasoning_text`, and `reasoning_content` to `reasoning_content`.\r\n- When thinking is enabled and an assistant message contains tool calls, add `reasoning_content: \"\"` if missing.\r\n\r\n## Files\r\n- packages/ai/src/providers/openai-completions.ts\r\n\r\n## Validation\r\n- `npm run check` passes.\r\n- Reproduced the failing session locally and confirmed the converted assistant tool-call message now includes `reasoning_content`.\r\n","headRefName":"fix/kimi","number":987,"state":"OPEN","title":"fix(ai): include reasoning_content for OpenRouter Kimi K2.5 tool calls"},{"body":"Fixes #916\n\nAdds an `autocompleteMaxVisible` setting (range 3-20, default 5) to control how many items are visible in the editor's autocomplete dropdown.\n\n\n\n### Changes\n- `packages/tui/src/components/editor.ts`: Add `autocompleteMaxVisible` to `EditorOptions`, store as field, use in `SelectList` creation instead of hardcoded `5`\n- `packages/coding-agent/src/core/settings-manager.ts`: Add to `Settings` interface, add getter/setter\n- `packages/coding-agent/src/modes/interactive/components/settings-selector.ts`: Add UI toggle with presets [3, 5, 7, 10, 15, 20]\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`: Pass setting to editor, wire up callback\n- `packages/coding-agent/docs/settings.md`: Document in settings table","headRefName":"feat/autocomplete-max-visible","number":972,"state":"OPEN","title":"feat: add autocompleteMaxVisible setting for configurable dropdown height"},{"body":" ## Summary\r\n Add a minimal `skills_discover` extension hook to the ResourceLoader pipeline, plus an example extension that walks upward to discover `.pi/skills`.\r\n\r\n ## Motivation\r\n The v0.50.0 loader architecture (#645) is flexible but still doesn’t allow extensions to contribute additional skill directories during discovery (e.g., ancestor traversal). This hook enables that without changing default discovery rules.\r\n\r\n ## What’s Included\r\n - New `skills_discover` hook surfaced in the extension API and runner.\r\n - ResourceLoader calls `skills_discover` before loading skills and merges any returned directories.\r\n - Example extension: upward discovery of `.pi/skills` via `skills_discover`.\r\n\r\n ## What’s NOT Included\r\n - No changes to default skill discovery behavior.\r\n - No changelog edits (per CONTRIBUTING.md).\r\n\r\n ## Tests\r\n - `npm run check` (ran; auto‑formatted one comment in `extensions/runner.ts`)\r\n - `./test.sh` (ran; no failed tests, some skipped as expected)\r\n - `npm run build` (ran)\r\n\r\n Note: `npm test` output includes expected errors from package‑manager tests that hit `nonexistent-package` / `nonexistent` git repo; the test run still completed with no failing test cases.","headRefName":"feat-skills-discover-hook","number":965,"state":"OPEN","title":"coding-agent: add skills_discover hook + upward .pi/skills example"},{"body":"Might or might be useful. Useful to me :)","headRefName":"session-metadata-change-event","number":964,"state":"OPEN","title":"Session metadata change event"},{"body":"Adds a Qwen CLI provider extension with OAuth device flow support, matching the official Qwen CLI.\r\n\r\nChanges:\r\n- Add custom-provider-qwen-cli extension (packages/coding-agent/examples/extensions/custom-provider-qwen-cli)\r\n- Add Qwen OAuth device flow with PKCE and refresh support\r\n- Register Qwen models (coder plus/flash + vision) with OpenAI-compatible API\r\n- Add thinkingFormat: \"qwen\" to core types (uses enable_thinking param)\r\n- Enable modifyModels hook for extension OAuth providers (documented in packages/coding-agent/docs/custom-provider.md but not implemented; enables dynamic baseUrl from OAuth credentials)\r\n- Register extension in example list + workspace\r\n\r\nModels:\r\n- qwen3-coder-plus: 1M context, 65K output\r\n- qwen3-coder-flash: 1M context, 65K output\r\n- vision-model (qwen3-vl-plus): 262K context, 32K output, vision + thinking\r\n\r\nTesting:\r\n- npm run check passes\r\n- ./test.sh passes\r\n- OAuth flow verified against official Qwen CLI source\r\n- API calls and thinking mode tested with live tokens","headRefName":"feat/qwen-cli-extension","number":940,"state":"OPEN","title":"feat(coding-agent): add Qwen CLI OAuth provider"},{"body":"Adds support for rendering inline images inside tmux using the Kitty graphics protocol's unicode placeholder mode.\n\n## Problem\n\nWhen running pi inside tmux + Ghostty/Kitty/WezTerm:\n- Direct Kitty graphics sequences get eaten by tmux (without passthrough) or cause \"stuck\" images (with passthrough)\n- Images orphan on screen redraws, pane switches, or scrolling\n\n## Solution\n\nImplement the protocol's multiplexer-safe mode:\n- Detect tmux via `TMUX` env var\n- Check `allow-passthrough` setting before attempting image display\n- Wrap sequences in tmux passthrough escapes\n- Use virtual placement (`U=1`) with unicode placeholders (`U+10EEEE`)\n- Encode row positions via diacritics (per official spec)\n\n## Performance Optimizations\n\nTo minimize tmux passthrough overhead:\n- **Transmit-once**: Image data is transmitted only on first render; subsequent redraws reuse placeholders (image stays in terminal memory by ID)\n- **Wrap-once**: All Kitty chunks are batched into a single tmux passthrough frame instead of wrapping each 4KB chunk individually\n- **Sync passthrough**: Synchronized output markers (`CSI ?2026 h/l`) are passed through to the outer terminal, enabling batched rendering instead of progressive scroll\n\n## Backward Compatibility\n\n- Outside tmux: behavior unchanged, direct placement as before\n- Inside tmux without `allow-passthrough`: gracefully falls back to text description (`[Image: foo.png 800x600]`)\n- Inside tmux with `allow-passthrough all`: full image rendering\n\n## User Requirement\n\nFor images to render in tmux, users need in their tmux.conf:\n```\nset -g allow-passthrough all\n```","headRefName":"feat/tmux-kitty-images","number":908,"state":"OPEN","title":"feat(tui): add tmux support for kitty graphics protocol"},{"body":"Discussion:\r\n\r\n<img width=\"1020\" height=\"350\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a2a1633a-acee-40d8-a7ba-2d2ab9808514\" />\r\n\r\nThis allows hooking bash execution and it also allows `tool_result` hooks to manipulate error messages by \"abusing\" content to carry error messages. This feels somewhat consistent to how it works but a bit ugly.","headRefName":"intercept-bash","number":903,"state":"OPEN","title":"Add support for intercepting bash"},{"body":"@assagman I will pull your doc changes in here too and adapt them to whatever becomes the final idea here.\r\n\r\nBasically what I find annoying is that z.ai API keys work for both API and Coding Plan. The only difference is the endpoint itself. The way it currently is, whenever someone has `ZAI_API_KEY` defined, they will have both providers available, which is a bit noisy for what I assume to be the most cases of people only using one option.\r\n\r\nThe ideas that occurred to me were:\r\n\r\n1. Introduce a different environment var e.g. `ZAI_CODING_API_KEY` so that you know which provider you are configuring. The only caveat is that every other tool I encountered uses `ZAI_API_KEY` for the config\r\n2. Replace the `zai` provider entirely with `zai-coding-plan`\r\n\r\nI personally prefer 2 as it keeps things simpler while making it more aligned with models.dev and being explicit that we are supporting the Coding Plan specifically and not the regular API. So far, pi only worked with the coding plan anyways, since it always used coding plan endpoints. I can add examples to the docs on how to setup the regular API by hand. \r\n\r\nFrom the fact that it has always been Coding Plan and no one complained, I assume there are not many API users out there. API users tend to prefer other providers and even avoid zai, from what I've seen.","headRefName":"zai-coding-plan-provider","number":899,"state":"OPEN","title":"Add zai-coding-plan provider and make zai point to regular API"},{"body":"adds `{{tools}}`, `{{context}}`, `{{skills}}` template variables for SYSTEM.md.\n\nwhen present, replaced with dynamic content. no templates = full replacement (current behavior preserved).\n\nalso tracks injection metadata through sdk → session → ui, so the welcome screen only shows \"loaded context/skills\" when actually injected into the system prompt.","headRefName":"feat/system-prompt-template-variables","number":892,"state":"OPEN","title":"feat(coding-agent): add template variables to custom system prompts"},{"body":"Originating issue: https://github.com/badlogic/pi-mono/issues/862\r\n\r\nThe `/resume` picker already supports search, scope toggling, and sort modes, but named sessions can get lost easily sometimes. This adds a `Ctrl+N` toggle to filter the list down to sessions named via `/name`, while keeping the existing search/sort behavior intact (name filtering applies even when the query is empty).\r\n\r\nSummary of changes:\r\n- add `NameFilter` (`all | named`) to `filterAndSortSessions()` and apply it before query parsing/matching\r\n- add `Ctrl+N` in the session selector to toggle `Name: All` ↔ `Name: Named`, and show current state in the header\r\n- improve empty-state messaging when the named filter is active (including the escape hatch key)\r\n- tests: add `name filter` coverage (incl. whitespace-only names excluded via `trim()`)\r\n- add configurable app keybinding `toggleSessionNamedFilter` (default: `ctrl+n`) via `~/.pi/agent/keybindings.json`\r\n- session selector now uses `KeybindingsManager` for the named-filter toggle and UI hints/empty-state messaging, so users can remap it (e.g. for Emacs/readline `ctrl+n` = down)\r\n\r\nFiles:\r\n- `packages/coding-agent/README.md`\r\n- `packages/coding-agent/src/cli/session-picker.ts`\r\n- `packages/coding-agent/src/core/keybindings.ts`\r\n- `packages/coding-agent/src/modes/interactive/components/session-selector-search.ts`\r\n- `packages/coding-agent/src/modes/interactive/components/session-selector.ts`\r\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`\r\n- `packages/coding-agent/test/session-selector-path-delete.test.ts`\r\n- `packages/coding-agent/test/session-selector-search.test.ts`\r\n\r\nManual testing:\r\n- `/resume`: Ctrl+N toggles All/Named; header updates; hint shows `ctrl+n: named`\r\n- `/resume`: named filter works with empty search query and with an active query\r\n- `/resume`: empty state mentions Ctrl+N when no named sessions are available in the current scope\r\n- `/resume`: named filter toggle works; header updates; hint shows the configured keybinding\r\n- `keybindings.json`: remapping `toggleSessionNamedFilter` works (e.g. free up `ctrl+n` for `selectDown`)\r\n\r\n<p align=\"center\">\r\n<img width=\"360\" alt=\"all\" src=\"https://github.com/user-attachments/assets/5d7908d9-dba2-4067-85d3-735f84723ce7\" />\r\n\r\n<img width=\"360\" alt=\"named\" src=\"https://github.com/user-attachments/assets/ccf1f969-79b9-439f-b5b6-53731a777e90\" />\r\n</p>","headRefName":"session-named-filter","number":868,"state":"OPEN","title":"feat(coding-agent): add named-only filter toggle to /resume picker"}]I found PR #1018 which is related to issue #1019. Let me get more details about that PR.
Found the related PR #1018. Let me get its details:
>gh pr view 1018 --repo badlogic/pi-mono --json title,body,comments,state,commits,files,additions,deletions,headRefName,reviews{"additions":8,"body":"Fixes #1019\n\nPreviously, within a single tool-use turn, rate limit retries would accumulate across separate LLM calls. For example, if each of 3 tool calls hit a 429 and retried once, the counter would show '3/3' and fail even though each individual retry succeeded.\n\nNow the counter resets immediately when a successful (non-error) assistant message arrives, so each LLM call gets a fresh set of retries.\n\n## Changes\n- Reset counter in `message_end` handler for successful assistant messages (key fix)\n- Reset counter at start of `prompt()` for new user prompts (safety net)\n- Reset counter unconditionally at `agent_end` (safety net)\n\n## Testing\n- Manual testing with rate-limited provider (Baseten) during multi-tool-call turns","comments":[{"id":"IC_kwDOPbFNk87jA4Bn","author":{"login":"github-actions"},"authorAssociation":"CONTRIBUTOR","body":"Hi @charles-cooper, thanks for your interest in contributing!\n\nWe ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.\n\n**Next steps:**\n1. Open an issue describing what you want to change and why (keep it concise, write in your human voice, AI slop will be closed)\n2. Once a maintainer approves with `lgtm`, you'll be added to the approved contributors list\n3. Then you can submit your PR\n\nThis PR will be closed automatically. See https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md for more details.","createdAt":"2026-01-28T02:50:45Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/1018#issuecomment-3808657511","viewerDidAuthor":false}],"commits":[{"authoredDate":"2026-01-28T02:47:07Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjM4Njc1MDE=","login":"charles-cooper","name":"Charles Cooper"}],"committedDate":"2026-01-28T02:47:07Z","messageBody":"Previously, the retry counter persisted across separate user prompts.\nIf 2 retries failed, then the session resumed with more messages, the\nnext rate limit error would show '3/3' and fail immediately instead of\nstarting fresh.\n\nNow the counter resets when a new user prompt starts (not via retry),\nso each prompt gets a fresh set of retries.","messageHeadline":"fix(coding-agent): reset retry counter on new user prompts","oid":"730f9ec1d52f039076857625576f8c58a13a9a5f"}],"deletions":0,"files":[{"path":"packages/coding-agent/CHANGELOG.md","additions":1,"deletions":0},{"path":"packages/coding-agent/src/core/agent-session.ts","additions":7,"deletions":0}],"headRefName":"feat/openai-retry","reviews":[],"state":"CLOSED","title":"fix(coding-agent): reset retry counter after each successful LLM response"}>gh pr diff 1018 --repo badlogic/pi-monodiff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index a121a117c..6cd0d504a 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -105,6 +105,7 @@ There are multiple SDK breaking changes since v0.49.3. For the quickest migratio
- Off-by-one error in bash output "earlier lines" count caused by counting spacing newline as hidden content ([#921](https://github.com/badlogic/pi-mono/issues/921))
- User package filters now layer on top of manifest filters instead of replacing them ([#645](https://github.com/badlogic/pi-mono/issues/645))
- Auto-retry now handles "terminated" errors from Codex API mid-stream failures
+- Fixed auto-retry counter accumulating across separate user prompts. Previously, if 2 retries failed and the session resumed, the next rate limit error would show "3/3" and fail immediately instead of starting fresh.
- Follow-up queue (Alt+Enter) now sends full paste content instead of `[paste #N ...]` markers ([#912](https://github.com/badlogic/pi-mono/issues/912))
- Fixed Alt-Up not restoring messages queued during compaction ([#923](https://github.com/badlogic/pi-mono/pull/923) by [@aliou](https://github.com/aliou))
- Fixed session corruption when loading empty or invalid session files via `--session` flag ([#932](https://github.com/badlogic/pi-mono/issues/932) by [@armanddp](https://github.com/armanddp))
diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts
index e5872290e..00bb147c1 100644
--- a/packages/coding-agent/src/core/agent-session.ts
+++ b/packages/coding-agent/src/core/agent-session.ts
@@ -716,6 +716,13 @@ export class AgentSession {
);
}
+ // Reset retry counter for new user prompts (not retries)
+ // This prevents retry failures from accumulating across separate prompts
+ if (this._retryAttempt > 0) {
+ this._retryAttempt = 0;
+ this._resolveRetry();
+ }
+
// Check if we need to compact before sending (catches aborted responses)
const lastAssistant = this._findLastAssistantMessage();
if (lastAssistant) {Now let me understand the issue better. The PR was closed (likely by the bot for new contributors). The issue describes:
The PR only resets the counter on new user prompts, but the issue says the counter should reset after each successful LLM response within a turn.
Let me read the agent-session.ts file to understand the current retry logic.
Let me read the agent-session.ts file to understand the current retry logic:
Let me continue reading the file to see the retry logic.
Now I understand the issue. Let me analyze:
Bug Description: The auto-retry counter accumulates across separate LLM calls within a single turn.
Current Behavior (from the code):
In _handleAgentEvent, on agent_end, if the message is NOT a retryable error but _retryAttempt > 0, it resets the counter:
} else if (this._retryAttempt > 0) { // Previous retry succeeded - emit success event and reset counter this._emit({...}); this._retryAttempt = 0; this._resolveRetry();}This means the counter is ONLY reset when agent_end fires with a successful message AND there was a previous retry attempt.
The Problem: In a tool-use turn with multiple LLM calls, the flow is:
_retryAttempt = 1 → retries → succeedsagent_end fire here? No, the agent continues processing tool results and makes another call_retryAttempt = 2 (accumulates!)_retryAttempt = 3_retryAttempt > maxRetries → failsThe counter only resets on agent_end with a successful response, but during a multi-tool turn, agent_end doesn't fire between individual LLM calls.
The Fix Proposed in Issue:
Reset _retryAttempt immediately when a successful (non-error) assistant message arrives in the message_end handler, not just at agent_end.
The PR's approach resets the counter at the start of prompt(), which handles accumulation across separate user prompts but misses the core issue—accumulation within a single turn when multiple LLM calls occur. The actual solution needs to clear the counter in _handleAgentEvent when message_end fires for a successful assistant message, allowing each LLM call within the same turn to start fresh. }
Looking at the agent event handler, when an agent completes, I need to check if there's a stored assistant message and handle retryable errors first. If the message contains a retryable error like overload or rate limiting, I'll attempt a retry and exit early. Otherwise, if a previous retry succeeded, I should emit a success event and reset the counter before proceeding to check for auto-compaction.
The key insight is that this reset only happens at `agent_end`. During a tool-use turn, the flow is: user sends prompt → `agent_start` fires → LLM responds → `message_end` fires and stores the assistant message → `turn_end` fires → tools execute → tool result gets added → `turn_start` fires triggering another LLM call.
This cycle repeats until `agent_end` fires when there are no more tool calls. Within a single agent run spanning multiple LLM calls, each could encounter rate limits, but the counter only resets at the very end. The solution is to reset after each successful `message_end` for assistant messages instead of waiting for `agent_end`. Since `message_end` fires for every LLM response, the fix belongs in that handler.Now I have a complete picture. Let me analyze:
When a tool-use turn involves multiple LLM calls (e.g., tool call 1 → tool result → tool call 2 → tool result → ...), each LLM call that hits a rate limit increments _retryAttempt. The counter only resets on agent_end, but agent_end fires once per user prompt, not between individual LLM calls within a turn.
Example scenario:
agent_start_retryAttempt = 1 → retry → succeeds_retryAttempt = 2 → retry → succeeds_retryAttempt = 3 → exceeds max (3) → failsagent_end fires (too late, we already failed)Each individual LLM call succeeded after one retry, but the counter never reset between calls.
if (event.type === "agent_end" && this._lastAssistantMessage) { // ... if (this._isRetryableError(msg)) { const didRetry = await this._handleRetryableError(msg); if (didRetry) return; } else if (this._retryAttempt > 0) { // Previous retry succeeded - reset only happens here at agent_end this._retryAttempt = 0; this._resolveRetry(); }}The PR resets counter at prompt() start:
if (this._retryAttempt > 0) { this._retryAttempt = 0; this._resolveRetry();}This fixes accumulation across separate user prompts, but NOT the issue described in #1019 (accumulation within a single turn).
Reset _retryAttempt in the message_end handler when receiving a successful assistant message (not just at agent_end).
Location: packages/coding-agent/src/core/agent-session.ts, in _handleAgentEvent, within the message_end block (~lines 309-330)
Change: Add retry counter reset when an assistant message arrives successfully:
// Track assistant message for auto-compaction (checked on agent_end)if (event.message.role === "assistant") { this._lastAssistantMessage = event.message; // Reset retry counter on successful assistant response // This prevents accumulation across multiple LLM calls within a turn const assistantMsg = event.message as AssistantMessage; if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) { this._emit({ type: "auto_retry_end", success: true, attempt: this._retryAttempt, }); this._retryAttempt = 0; this._resolveRetry(); }}The existing agent_end handler reset can become a safety net (it will rarely trigger since message_end already resets).
packages/coding-agent/src/core/agent-session.ts - the fixpackages/coding-agent/CHANGELOG.md - document the fixoki, implement, commit and push (and close issue), the comment in my otne: "Thanks for reporting! Fixed in main"
The user wants me to implement the fix, commit, push, and close the issue with a comment. Let me do this step by step.
First, I need to make the code change in agent-session.ts. Looking at the current code around line 309-330, I need to add the retry counter reset in the message_end handler for assistant messages.
Let me read the specific section again to get the exact text for the edit.
Let me continue reading to find the exact code section I need to modify.
Now I have the exact code I need to modify. Let me implement the fix:
In the message_end handler, after tracking the assistant message, I need to reset the retry counter if it's a successful response.
The agent_end handler can stay as a safety net.
Let me also read the CHANGELOG to add the entry.
Now let me read the CHANGELOG and make the edits:
Now I have the context. Let me make the edits:
### Fixed section in [Unreleased]Let me do both edits.
Now let me run the check command to make sure everything compiles correctly.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 442 files in 302ms. No fixes applied.
> @mariozechner/pi-web-ui@0.50.1 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Now let me commit, push, and close the issue with a comment.
Checks pass. Now commit, push, and close the issue:
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/core/agent-session.ts packages/coding-agent/CHANGELOG.md && git commit -m "fix(coding-agent): reset retry counter after each successful LLM response
Previously, within a single tool-use turn, rate limit retries would
accumulate across separate LLM calls. For example, if each of 3 tool
calls hit a 429 and retried once, the counter would show '3/3' and fail
even though each individual retry succeeded.
Now the counter resets immediately when a successful (non-error)
assistant message arrives, so each LLM call gets a fresh set of retries.
Fixes #1019"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 442 files in 275ms. No fixes applied.
> @mariozechner/pi-web-ui@0.50.1 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main 4f004ade] fix(coding-agent): reset retry counter after each successful LLM response
2 files changed, 14 insertions(+), 10 deletions(-)Committed. Now let me push and close the issue.
>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
25707f9a..4f004ade main -> main>gh issue close 1019 --repo badlogic/pi-mono --comment "Thanks for reporting! Fixed in main."✓ Closed issue badlogic/pi-mono#1019 (Auto-retry counter accumulates across LLM calls within a turn)Done! The issue is fixed, committed, pushed, and closed with a comment.
Done. Summary:
Fix: Reset _retryAttempt in message_end handler when receiving a successful assistant message, not just at agent_end. This prevents accumulation across multiple LLM calls within a single tool-use turn.
Changes:
packages/coding-agent/src/core/agent-session.ts - added retry reset in message_end handler, removed redundant reset from agent_endpackages/coding-agent/CHANGELOG.md - documented the fixCommit: 4f004ade - pushed to main, issue #1019 closed.
} else if (this._retryAttempt > 0) { // Previous retry succeeded - emit success event and reset counter this._emit({...}); this._retryAttempt = 0; this._resolveRetry();}
Looking at the agent event handler, when an agent completes, I need to check if there's a stored assistant message and handle retryable errors first. If the message contains a retryable error like overload or rate limiting, I'll attempt a retry and exit early. Otherwise, if a previous retry succeeded, I should emit a success event and reset the counter before proceeding to check for auto-compaction.
The key insight is that this reset only happens at `agent_end`. During a tool-use turn, the flow is: user sends prompt → `agent_start` fires → LLM responds → `message_end` fires and stores the assistant message → `turn_end` fires → tools execute → tool result gets added → `turn_start` fires triggering another LLM call.
This cycle repeats until `agent_end` fires when there are no more tool calls. Within a single agent run spanning multiple LLM calls, each could encounter rate limits, but the counter only resets at the very end. The solution is to reset after each successful `message_end` for assistant messages instead of waiting for `agent_end`. Since `message_end` fires for every LLM response, the fix belongs in that handler.if (event.type === "agent_end" && this._lastAssistantMessage) { // ... if (this._isRetryableError(msg)) { const didRetry = await this._handleRetryableError(msg); if (didRetry) return; } else if (this._retryAttempt > 0) { // Previous retry succeeded - reset only happens here at agent_end this._retryAttempt = 0; this._resolveRetry(); }}if (this._retryAttempt > 0) { this._retryAttempt = 0; this._resolveRetry();}// Track assistant message for auto-compaction (checked on agent_end)if (event.message.role === "assistant") { this._lastAssistantMessage = event.message; // Reset retry counter on successful assistant response // This prevents accumulation across multiple LLM calls within a turn const assistantMsg = event.message as AssistantMessage; if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) { this._emit({ type: "auto_retry_end", success: true, attempt: this._retryAttempt, }); this._retryAttempt = 0; this._resolveRetry(); }}