detached a2cd138

Retry Codex server capacity errors

commit
a2cd138cb3441062216614e8ead001bc29404d7a
author
patriceckhart · · 2026-07-30 09:22 UTC
parent
b289bf6
signature
unsigned
refs
tag: v0.3.27
2 files · +67 -16
M packages/core/agent.go +6 -0
@@ -478,6 +478,12 @@ func (a *Agent) canRetryError(err error, attempt int) bool {
478478 // Explicit retry guidance emitted by provider backends (OpenAI
479479 // Responses, AWS Bedrock stream exceptions) with varying prefixes.
480480 "you can retry your request", "try your request again", "please retry your request",
481+ // Capacity messages from the ChatGPT/Codex backend, e.g.
482+ // "Our servers are currently overloaded. Please try again later."
483+ // The trailing advice also shows up on its own for transient
484+ // capacity failures; usage/quota limits are filtered out above by
485+ // isNonRetryableProviderLimit before this list is consulted.
486+ "servers are currently overloaded", "servers are busy", "try again later",
481487 }
482488 for _, needle := range needles {
483489 if strings.Contains(msg, needle) {
@@ -478,6 +478,12 @@ func (a *Agent) canRetryError(err error, attempt int) bool {
478 // Explicit retry guidance emitted by provider backends (OpenAI478 // Explicit retry guidance emitted by provider backends (OpenAI
479 // Responses, AWS Bedrock stream exceptions) with varying prefixes.479 // Responses, AWS Bedrock stream exceptions) with varying prefixes.
480 "you can retry your request", "try your request again", "please retry your request",480 "you can retry your request", "try your request again", "please retry your request",
481+ // Capacity messages from the ChatGPT/Codex backend, e.g.
482+ // "Our servers are currently overloaded. Please try again later."
483+ // The trailing advice also shows up on its own for transient
484+ // capacity failures; usage/quota limits are filtered out above by
485+ // isNonRetryableProviderLimit before this list is consulted.
486+ "servers are currently overloaded", "servers are busy", "try again later",
481 }487 }
482 for _, needle := range needles {488 for _, needle := range needles {
483 if strings.Contains(msg, needle) {489 if strings.Contains(msg, needle) {
M packages/core/agent_retry_test.go +61 -16
@@ -2,6 +2,7 @@ package core
22
33 import (
44 "context"
5+ "errors"
56 "fmt"
67 "strings"
78 "sync/atomic"
@@ -65,11 +66,11 @@ func TestAgentRetriesOverloadedStreamError(t *testing.T) {
6566 }
6667 }
6768
68-// codexRetryFakeClient reproduces the generic OpenAI Codex backend
69-// failure ("An error occurred while processing your request. You can
70-// retry your request, ...") on the first call and succeeds afterwards.
69+// codexRetryFakeClient reproduces a transient OpenAI Codex backend
70+// failure on the first call and succeeds afterwards.
7171 type codexRetryFakeClient struct {
72- calls int32
72+ calls int32
73+ firstErr string
7374 }
7475
7576 func (c *codexRetryFakeClient) Name() string { return "openai-codex" }
@@ -81,7 +82,7 @@ func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request)
8182 defer close(out)
8283 out <- provider.EventStart{Provider: "openai-codex", Model: req.Model}
8384 if call == 1 {
84- out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.")}
85+ out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("%s", c.firstErr)}
8586 return
8687 }
8788 out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
@@ -93,19 +94,63 @@ func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request)
9394 }
9495
9596 func TestAgentRetriesCodexProcessingError(t *testing.T) {
96- client := &codexRetryFakeClient{}
97- a := NewAgent(client, "gpt-5.6-sol", "system", Registry{})
98- a.RetryBaseDelay = time.Millisecond
99-
100- if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
101- t.Fatalf("Prompt returned %v; want retry to succeed", err)
97+ cases := []struct {
98+ name string
99+ err string
100+ }{
101+ {
102+ name: "processing error",
103+ err: "codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.",
104+ },
105+ {
106+ name: "servers overloaded",
107+ err: "codex error: Our servers are currently overloaded. Please try again later.",
108+ },
109+ {
110+ name: "try again later only",
111+ err: "codex error: Something went wrong. Please try again later.",
112+ },
102113 }
103- if got := atomic.LoadInt32(&client.calls); got != 2 {
104- t.Fatalf("Stream calls = %d; want 2", got)
114+ for _, tc := range cases {
115+ t.Run(tc.name, func(t *testing.T) {
116+ client := &codexRetryFakeClient{firstErr: tc.err}
117+ a := NewAgent(client, "gpt-5.6-sol", "system", Registry{})
118+ a.RetryBaseDelay = time.Millisecond
119+
120+ if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
121+ t.Fatalf("Prompt returned %v; want retry to succeed", err)
122+ }
123+ if got := atomic.LoadInt32(&client.calls); got != 2 {
124+ t.Fatalf("Stream calls = %d; want 2", got)
125+ }
126+ msgs := a.Messages()
127+ if len(msgs) != 2 || extractText(msgs[1]) != "ok" {
128+ t.Fatalf("messages = %v; want user + ok assistant", msgs)
129+ }
130+ })
105131 }
106- msgs := a.Messages()
107- if len(msgs) != 2 || extractText(msgs[1]) != "ok" {
108- t.Fatalf("messages = %v; want user + ok assistant", msgs)
132+}
133+
134+// TestCanRetryErrorCapacityMessages pins the classification of Codex
135+// capacity wording and makes sure quota/usage limits stay terminal even
136+// when they carry "try again later" style advice.
137+func TestCanRetryErrorCapacityMessages(t *testing.T) {
138+ a := NewAgent(nil, "gpt-5.6-sol", "system", Registry{})
139+ cases := []struct {
140+ msg string
141+ want bool
142+ }{
143+ {"codex error: Our servers are currently overloaded. Please try again later.", true},
144+ {"codex error: Our servers are busy right now.", true},
145+ {"codex error: Please try again later.", true},
146+ {"codex error: You have hit your monthly usage limit. Try again later.", false},
147+ {"codex error: quota exceeded, try again later", false},
148+ {"codex error: unsupported parameter: reasoning", false},
149+ }
150+ for _, tc := range cases {
151+ if got := a.canRetryError(errors.New(tc.msg), 0); got != tc.want {
152+ t.Errorf("canRetryError(%q) = %v; want %v", tc.msg, got, tc.want)
153+ }
109154 }
110155 }
111156
@@ -2,6 +2,7 @@ package core
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "fmt"6 "fmt"
6 "strings"7 "strings"
7 "sync/atomic"8 "sync/atomic"
@@ -65,11 +66,11 @@ func TestAgentRetriesOverloadedStreamError(t *testing.T) {
65 }66 }
66 }67 }
67 68
68-// codexRetryFakeClient reproduces the generic OpenAI Codex backend69+// codexRetryFakeClient reproduces a transient OpenAI Codex backend
69-// failure ("An error occurred while processing your request. You can70+// failure on the first call and succeeds afterwards.
70-// retry your request, ...") on the first call and succeeds afterwards.
71 type codexRetryFakeClient struct {71 type codexRetryFakeClient struct {
72- calls int3272+ calls int32
73+ firstErr string
73 }74 }
74 75
75 func (c *codexRetryFakeClient) Name() string { return "openai-codex" }76 func (c *codexRetryFakeClient) Name() string { return "openai-codex" }
@@ -81,7 +82,7 @@ func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request)
81 defer close(out)82 defer close(out)
82 out <- provider.EventStart{Provider: "openai-codex", Model: req.Model}83 out <- provider.EventStart{Provider: "openai-codex", Model: req.Model}
83 if call == 1 {84 if call == 1 {
84- out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.")}85+ out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("%s", c.firstErr)}
85 return86 return
86 }87 }
87 out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{88 out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
@@ -93,19 +94,63 @@ func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request)
93 }94 }
94 95
95 func TestAgentRetriesCodexProcessingError(t *testing.T) {96 func TestAgentRetriesCodexProcessingError(t *testing.T) {
96- client := &codexRetryFakeClient{}97+ cases := []struct {
97- a := NewAgent(client, "gpt-5.6-sol", "system", Registry{})98+ name string
98- a.RetryBaseDelay = time.Millisecond99+ err string
99-100+ }{
100- if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {101+ {
101- t.Fatalf("Prompt returned %v; want retry to succeed", err)102+ name: "processing error",
103+ err: "codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.",
104+ },
105+ {
106+ name: "servers overloaded",
107+ err: "codex error: Our servers are currently overloaded. Please try again later.",
108+ },
109+ {
110+ name: "try again later only",
111+ err: "codex error: Something went wrong. Please try again later.",
112+ },
102 }113 }
103- if got := atomic.LoadInt32(&client.calls); got != 2 {114+ for _, tc := range cases {
104- t.Fatalf("Stream calls = %d; want 2", got)115+ t.Run(tc.name, func(t *testing.T) {
116+ client := &codexRetryFakeClient{firstErr: tc.err}
117+ a := NewAgent(client, "gpt-5.6-sol", "system", Registry{})
118+ a.RetryBaseDelay = time.Millisecond
119+
120+ if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
121+ t.Fatalf("Prompt returned %v; want retry to succeed", err)
122+ }
123+ if got := atomic.LoadInt32(&client.calls); got != 2 {
124+ t.Fatalf("Stream calls = %d; want 2", got)
125+ }
126+ msgs := a.Messages()
127+ if len(msgs) != 2 || extractText(msgs[1]) != "ok" {
128+ t.Fatalf("messages = %v; want user + ok assistant", msgs)
129+ }
130+ })
105 }131 }
106- msgs := a.Messages()132+}
107- if len(msgs) != 2 || extractText(msgs[1]) != "ok" {133+
108- t.Fatalf("messages = %v; want user + ok assistant", msgs)134+// TestCanRetryErrorCapacityMessages pins the classification of Codex
135+// capacity wording and makes sure quota/usage limits stay terminal even
136+// when they carry "try again later" style advice.
137+func TestCanRetryErrorCapacityMessages(t *testing.T) {
138+ a := NewAgent(nil, "gpt-5.6-sol", "system", Registry{})
139+ cases := []struct {
140+ msg string
141+ want bool
142+ }{
143+ {"codex error: Our servers are currently overloaded. Please try again later.", true},
144+ {"codex error: Our servers are busy right now.", true},
145+ {"codex error: Please try again later.", true},
146+ {"codex error: You have hit your monthly usage limit. Try again later.", false},
147+ {"codex error: quota exceeded, try again later", false},
148+ {"codex error: unsupported parameter: reasoning", false},
149+ }
150+ for _, tc := range cases {
151+ if got := a.canRetryError(errors.New(tc.msg), 0); got != tc.want {
152+ t.Errorf("canRetryError(%q) = %v; want %v", tc.msg, got, tc.want)
153+ }
109 }154 }
110 }155 }
111 156

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close