What happened
While investigating why the code agent produced the context.DeadlineExceeded bug in PR #6217, analysis revealed that the existing isTimeoutError function in internal/forge/github/github.go (around line 295) has the same vulnerability. It uses the Timeout() bool interface check without guarding against context errors.
The function's doc comment states "Callers must check ctx.Err() first," which shifts the burden to callers. The code agent replicated this pattern into the new public forge.IsTransient() function but did not replicate the caller obligation — creating the bug that the review agent caught.
The new IsTransient function now has the correct guard (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) before the Timeout() check). But isTimeoutError still relies on caller discipline.
What could go better
The caller-must-check-first contract on isTimeoutError is fragile. It already caused a bug by proxy — the code agent treated the function's implementation as a reusable pattern without carrying over the documented caller obligation. Any future caller (human or agent) that uses isTimeoutError without checking ctx.Err() first would silently retry context cancellations.
Confidence: Medium-high. The fragility is demonstrated by the PR #6217 bug. Making isTimeoutError self-contained (adding the context-error guard internally) eliminates the caller obligation and prevents the pattern from propagating incorrectly again.
Uncertainty: The function may have few callers, all of which correctly check ctx.Err() first. In that case the risk is low but the fix is also low-cost. There may also be a performance reason for the current design (avoiding redundant errors.Is checks), though this seems unlikely to matter in practice.
Proposed change
Add context-error guards to isTimeoutError in internal/forge/github/github.go, matching the pattern now used in forge.IsTransient():
func isTimeoutError(err error) bool {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return false
}
var te interface{ Timeout() bool }
return errors.As(err, &te) && te.Timeout()
}
Update the doc comment to remove the "Callers must check ctx.Err() first" obligation, since the function is now self-contained. Verify that existing callers still behave correctly (the added guard should be a no-op for callers that already check ctx.Err()).
Validation criteria
- All existing tests for
isTimeoutError and its callers continue to pass.
- A new test case verifies that
isTimeoutError(context.DeadlineExceeded) returns false.
- The doc comment no longer requires callers to pre-check
ctx.Err().
go vet and the race detector report no issues.
Generated by retro agent from #6217
What happened
While investigating why the code agent produced the
context.DeadlineExceededbug in PR #6217, analysis revealed that the existingisTimeoutErrorfunction ininternal/forge/github/github.go(around line 295) has the same vulnerability. It uses theTimeout() boolinterface check without guarding against context errors.The function's doc comment states "Callers must check ctx.Err() first," which shifts the burden to callers. The code agent replicated this pattern into the new public
forge.IsTransient()function but did not replicate the caller obligation — creating the bug that the review agent caught.The new
IsTransientfunction now has the correct guard (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)before theTimeout()check). ButisTimeoutErrorstill relies on caller discipline.What could go better
The caller-must-check-first contract on
isTimeoutErroris fragile. It already caused a bug by proxy — the code agent treated the function's implementation as a reusable pattern without carrying over the documented caller obligation. Any future caller (human or agent) that usesisTimeoutErrorwithout checkingctx.Err()first would silently retry context cancellations.Confidence: Medium-high. The fragility is demonstrated by the PR #6217 bug. Making
isTimeoutErrorself-contained (adding the context-error guard internally) eliminates the caller obligation and prevents the pattern from propagating incorrectly again.Uncertainty: The function may have few callers, all of which correctly check
ctx.Err()first. In that case the risk is low but the fix is also low-cost. There may also be a performance reason for the current design (avoiding redundanterrors.Ischecks), though this seems unlikely to matter in practice.Proposed change
Add context-error guards to
isTimeoutErrorininternal/forge/github/github.go, matching the pattern now used inforge.IsTransient():Update the doc comment to remove the "Callers must check ctx.Err() first" obligation, since the function is now self-contained. Verify that existing callers still behave correctly (the added guard should be a no-op for callers that already check
ctx.Err()).Validation criteria
isTimeoutErrorand its callers continue to pass.isTimeoutError(context.DeadlineExceeded)returnsfalse.ctx.Err().go vetand the race detector report no issues.Generated by retro agent from #6217