Files
kandev/apps/backend/internal/agentctl/server/adapter/codex_errors.go
T
Carlos Florêncio cff2906712 feat: setup and cleanup script (#80)
fix setup

error messages

feat: codex error handling
2026-01-23 23:36:04 +00:00

136 lines
4.1 KiB
Go

package adapter
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
)
// codexErrorRegex matches Codex error log format:
// TIMESTAMP ERROR module: error=HTTP_ERROR: Some("JSON")
var codexErrorRegex = regexp.MustCompile(`error=(.+?):\s*Some\("(.+)"\)\s*$`)
// CodexParsedError contains the parsed error information from Codex stderr.
type CodexParsedError struct {
// Message is the user-friendly error message
Message string
// HTTPError is the HTTP error string (e.g., "http 429 Too Many Requests")
HTTPError string
// RawJSON contains all fields from the error JSON (generic, captures any structure)
RawJSON map[string]any
// ErrorType is the error type from the JSON (e.g., "usage_limit_reached") - may be empty
ErrorType string
// ResetsInSeconds is the number of seconds until the limit resets - may be 0
ResetsInSeconds int64
}
// parseCodexStderrError attempts to parse a Codex stderr error line and extract
// error information. Returns nil if parsing fails.
//
// Example input:
//
// 2026-01-23T22:57:08.953223Z ERROR codex_api::endpoint::responses: error=http 429 Too Many Requests: Some("{\"error\":{...}}")
func parseCodexStderrError(line string) *CodexParsedError {
matches := codexErrorRegex.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
}
httpError := strings.TrimSpace(matches[1])
jsonStr := matches[2]
// Unescape the JSON string (it's double-escaped in the log)
unescaped := strings.ReplaceAll(jsonStr, `\"`, `"`)
unescaped = strings.ReplaceAll(unescaped, `\\`, `\`)
result := &CodexParsedError{
HTTPError: httpError,
}
// Try to parse the JSON into a generic map first (captures all fields)
var rawData map[string]any
if err := json.Unmarshal([]byte(unescaped), &rawData); err != nil {
// Couldn't parse JSON at all, just return HTTP error
result.Message = httpError
return result
}
result.RawJSON = rawData
// Extract common fields - check both nested "error" object and top-level fields
var errorType, errorMessage string
var resetsInSeconds int64
// First, check for nested error object (standard format)
if errObj, ok := rawData["error"].(map[string]any); ok {
if t, ok := errObj["type"].(string); ok {
errorType = t
}
if m, ok := errObj["message"].(string); ok {
errorMessage = m
}
// Handle both int and float64 (JSON numbers are float64 in Go)
if r, ok := errObj["resets_in_seconds"].(float64); ok {
resetsInSeconds = int64(r)
}
}
// Fall back to top-level fields if nested fields not found (flat error format)
if errorMessage == "" {
if m, ok := rawData["message"].(string); ok {
errorMessage = m
}
}
if errorType == "" {
if t, ok := rawData["type"].(string); ok {
errorType = t
}
}
result.ErrorType = errorType
result.ResetsInSeconds = resetsInSeconds
// Build a user-friendly message from available fields
var msg string
if errorMessage != "" {
// We have a specific error message from the API
msg = errorMessage
// Add reset time info if available
if resetsInSeconds > 0 {
duration := time.Duration(resetsInSeconds) * time.Second
if duration.Hours() >= 1 {
msg = fmt.Sprintf("%s (resets in %.0f hours)", msg, duration.Hours())
} else if duration.Minutes() >= 1 {
msg = fmt.Sprintf("%s (resets in %.0f minutes)", msg, duration.Minutes())
} else {
msg = fmt.Sprintf("%s (resets in %d seconds)", msg, int(duration.Seconds()))
}
}
} else if errorType != "" {
// We have an error type but no message
msg = fmt.Sprintf("Error: %s", errorType)
} else {
// Unknown error format - show HTTP error and include JSON body
jsonBytes, _ := json.MarshalIndent(rawData, "", " ")
msg = fmt.Sprintf("%s\n\n%s", httpError, string(jsonBytes))
}
result.Message = msg
return result
}
// parseCodexStderrLines attempts to parse Codex stderr lines and extract
// error information. Searches from the end (most recent) first.
// Returns nil if no parseable error is found.
func parseCodexStderrLines(lines []string) *CodexParsedError {
for i := len(lines) - 1; i >= 0; i-- {
if parsed := parseCodexStderrError(lines[i]); parsed != nil {
return parsed
}
}
return nil
}