v3.9.7 — Forward real codebuff error messages, fix BrokenPipeError crash, fix SyntaxWarnings

This commit is contained in:
Roman
2026-05-25 11:07:02 +04:00
Unverified
parent e819aaef8a
commit 528b3e65ee
5 changed files with 165 additions and 292 deletions

View File

@@ -26,7 +26,38 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.8.3", "2026-05-24", [
("3.9.7", "2026-05-25", [
"Forward real Codebuff error messages to user (not generic 429)",
"Return HTTP 200 with Responses API format for rate limits so Codex displays message",
"Extract retryAfterMs from Codebuff 429 responses for accurate cooldown",
"RateLimitError carries upstream message through session + chat error paths",
"BrokenPipeError crash fix on 'all accounts exhausted' response",
"Fix 3 SyntaxWarnings for invalid escape sequences in docstrings",
"_codebuff_start_run returns actual error body instead of None",
]),
("3.9.6", "2026-05-25", [
"Fix Gemini follow-up turns returning text-only instead of tool calls",
"Enforce latest user instruction as final Gemini content turn",
"Edit-intent detection with tool-use nudge for file modification requests",
"Debug logging: contents count, latest user text, final content preview",
"Thought signature preservation for Gemini 3 tool-call continuity",
"thought_signature field on all functionCall parts (snake_case)",
"Smart tool output compaction: old=3000, recent=20000 chars",
"Follow-through guardrail system instruction for autonomous agent behavior",
"Stream hang fix for function-call-only responses",
"Multi-account rotation for codebuff, Google OAuth, API keys",
"/v1/accounts endpoint for account pool status",
]),
("3.9.0", "2026-05-24", [
"Multi-account rotation for OAuth providers (codebuff, Google, API keys)",
"Automatic failover: when one account hits rate limit, next is used",
"Codebuff: supports accounts[] array in credentials.json",
"Google OAuth: supports multiple token files (google-*-oauth-token-N.json)",
"API keys: comma-separated keys rotate on 429 errors",
"New /v1/accounts endpoint shows account pool status",
"Added x-codebuff-model and x-codebuff-instance-id headers",
]),
("3.8.4", "2026-05-24", [
"FIXED: Codebuff streaming — SSE events now reach Codex client",
"Root cause: stream_buffered_events was never called for codebuff",
"Codebuff stream uses buffered flushing (30ms / 4KB / urgent)",
@@ -1706,7 +1737,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v3.8.3</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v3.9.7</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -3495,7 +3526,7 @@ class EditEndpointDialog(Gtk.Dialog):
auth_url = "https://codebuff.com/api/auth/cli/code"
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
req = urllib.request.Request(auth_url, data=body,
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.8.3"})
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.7"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3520,7 +3551,7 @@ class EditEndpointDialog(Gtk.Dialog):
time.sleep(2)
try:
poll_req = urllib.request.Request(poll_url,
headers={"User-Agent": "codex-launcher/3.8.3"})
headers={"User-Agent": "codex-launcher/3.9.7"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")

View File

@@ -70,9 +70,9 @@ FIX 6: Double-wrapped arguments (nested {"cmd": "{\"cmd\": \"curl...\"}"}")
FIX 7: _extract_field can't read values starting with \"
Symptom: sandbox_permissions="allow_all" passes through unnormalized because
_extract_field sees val_start=\ (backslash) which != " or { → returns None
_extract_field sees val_start=\\ (backslash) which != \" or { → returns None
Fix: Skip leading backslash before checking for " or { value type.
Location: _extract_field() leading-\ skip
Location: _extract_field() leading-backslash skip
FIX 8: Adaptive probing caused format mismatch (REVERTED)
Symptom: Probe system discovered OpenAI tool_calls+role=tool format but CC API couldn't
@@ -335,10 +335,31 @@ def _codebuff_get_session(token, model):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.9.0",
"User-Agent": "codex-launcher/3.9.7",
"x-codebuff-model": model,
})
resp = urllib.request.urlopen(req, timeout=15)
try:
resp = urllib.request.urlopen(req, timeout=15)
except urllib.error.HTTPError as e:
err_body = e.read().decode()[:1000]
if e.code == 429:
retry_s = 120
user_msg = ""
try:
err_data = json.loads(err_body)
retry_ms = err_data.get("retryAfterMs", 0)
if retry_ms:
retry_s = retry_ms / 1000
user_msg = err_data.get("message", err_data.get("error", ""))
if isinstance(user_msg, dict):
user_msg = user_msg.get("message", "")
except Exception:
pass
if not user_msg:
user_msg = _sanitize_err_body(err_body)
raise RateLimitError(retry_s, user_msg)
print(f"[codebuff] session HTTP {e.code}: {err_body[:200]}", file=sys.stderr)
return None
data = json.loads(resp.read())
instance_id = data.get("instanceId", data.get("data", {}).get("instance_id", ""))
expires_at = data.get("remainingMs", 0)
@@ -350,6 +371,8 @@ def _codebuff_get_session(token, model):
print(f"[codebuff] session active, instance={instance_id[:8]}...", file=sys.stderr)
return instance_id
return None
except RateLimitError:
raise
except Exception as e:
print(f"[codebuff] session failed: {e}", file=sys.stderr)
return None
@@ -360,21 +383,31 @@ def _codebuff_start_run(token, agent_id):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.9.0",
"User-Agent": "codex-launcher/3.9.7",
})
try:
resp = urllib.request.urlopen(req, timeout=15)
data = json.loads(resp.read())
run_id = data.get("runId")
print(f"[codebuff] started run {run_id} for agent {agent_id}", file=sys.stderr)
return run_id
return run_id, None
except urllib.error.HTTPError as e:
err = e.read().decode()[:300]
err = e.read().decode()[:500]
print(f"[codebuff] start run failed: HTTP {e.code}: {err}", file=sys.stderr)
return None
if e.code == 429:
retry_s = 120
try:
err_data = json.loads(err)
retry_ms = err_data.get("retryAfterMs", 0)
if retry_ms:
retry_s = retry_ms / 1000
except Exception:
pass
return None, ("rate_limit_error", 429, retry_s, _sanitize_err_body(err))
return None, ("upstream_error", e.code, 0, _sanitize_err_body(err))
except Exception as e:
print(f"[codebuff] start run error: {e}", file=sys.stderr)
return None
return None, ("proxy_error", 502, 0, str(e))
def _codebuff_finish_run(token, run_id, status="completed"):
url = f"{_FREEBUFF_API_URL}/api/v1/agent-runs"
@@ -383,7 +416,7 @@ def _codebuff_finish_run(token, run_id, status="completed"):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.9.0",
"User-Agent": "codex-launcher/3.9.7",
})
try:
urllib.request.urlopen(req, timeout=10)
@@ -392,6 +425,12 @@ def _codebuff_finish_run(token, run_id, status="completed"):
# ═══════════════════════════════════════════════════════════════════
# Multi-account rotation system
class RateLimitError(Exception):
def __init__(self, retry_seconds, message=""):
self.retry_seconds = retry_seconds
self.message = message
super().__init__(f"rate-limited for {retry_seconds:.0f}s: {message}")
# ═══════════════════════════════════════════════════════════════════
class AccountPool:
@@ -2804,7 +2843,7 @@ def _parse_commandcode_text_tool_calls(text):
Delegates to _extract_args() for the arguments field (handles unescaped + escaped JSON).
Delegates to _extract_field() for name/id/sandbox_permissions/justification
(with FIX 7 for leading-\ handling).
(with FIX 7 for leading-backslash handling).
Normalizes sandbox_permissions to valid values (use_default|require_escalated|with_user_approval)
[FIX 6] Prevents double-wrapped args: {"cmd": "{\"cmd\": \"curl...\"}"}
@@ -5209,13 +5248,30 @@ class Handler(http.server.BaseHTTPRequestHandler):
if attempt > 0:
print(f"[codebuff] rotation attempt {attempt+1}/{n_accounts}, trying account {acct_id}", file=sys.stderr)
run_id = _codebuff_start_run(token, agent_id)
run_id, run_err = _codebuff_start_run(token, agent_id)
if not run_id:
_fb_pool.mark_rate_limited(acct, 60)
last_err = ("upstream_error", 502, "Failed to start codebuff agent run. Check credentials and network.")
if run_err and run_err[0] == "rate_limit_error":
retry_s = run_err[2]
_fb_pool.mark_rate_limited(acct, retry_s)
last_err = ("rate_limit_error", run_err[1], f"Account {acct_id} rate-limited by Codebuff: {run_err[3]}")
else:
_fb_pool.mark_rate_limited(acct, 60)
last_err = ("upstream_error", run_err[1] if run_err else 502,
f"Failed to start agent run for {acct_id}: {run_err[3] if run_err else 'unknown error'}")
continue
instance_id = _codebuff_get_session(token, model)
try:
instance_id = _codebuff_get_session(token, model)
except RateLimitError as rle:
retry_s = rle.retry_seconds
fb_msg = rle.message
mins = int(retry_s // 60)
user_msg = fb_msg if fb_msg else f"Daily session limit reached. Resets in {mins}m."
print(f"[codebuff] session 429 for {acct_id}, retry after {retry_s:.0f}s", file=sys.stderr)
_fb_pool.mark_rate_limited(acct, retry_s)
_codebuff_finish_run(token, run_id, "completed")
last_err = ("rate_limit_error", 429, user_msg)
continue
input_data = body.get("input", "")
instructions = body.get("instructions", "").strip()
@@ -5249,7 +5305,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.9.0",
"User-Agent": "codex-launcher/3.9.7",
"x-codebuff-model": model,
}
if instance_id:
@@ -5266,14 +5322,22 @@ class Handler(http.server.BaseHTTPRequestHandler):
_codebuff_finish_run(token, run_id, "failed")
if e.code in (429, 426):
reset_ms = 0
fb_msg = ""
try:
err_json = json.loads(err_body)
reset_ms = err_json.get("retryAfterMs", 0)
fb_msg = err_json.get("message", err_json.get("error", ""))
if isinstance(fb_msg, dict):
fb_msg = fb_msg.get("message", "")
except Exception:
pass
duration = max(reset_ms / 1000, 120) if reset_ms else 120
mins = int(duration // 60)
if not fb_msg:
fb_msg = _sanitize_err_body(err_body)
user_msg = f"{fb_msg} (resets in {mins}m)" if fb_msg else f"Rate limited. Resets in {mins}m."
_fb_pool.mark_rate_limited(acct, duration)
last_err = ("upstream_error", e.code, _sanitize_err_body(err_body))
last_err = ("rate_limit_error", e.code, user_msg)
print(f"[codebuff] account {acct_id} got HTTP {e.code}, rotating", file=sys.stderr)
continue
if _is_reasoning_content_error(err_body):
@@ -5357,13 +5421,38 @@ class Handler(http.server.BaseHTTPRequestHandler):
return
if last_err:
return self.send_json(last_err[1], {"error": {"type": last_err[0], "message": f"All {n_accounts} accounts exhausted. {last_err[2]}"}})
msg = last_err[2]
resp_id = f"resp_{uuid.uuid4().hex[:24]}"
result = {
"id": resp_id,
"object": "response",
"created_at": int(time.time()),
"model": model,
"status": "completed",
"output": [{
"id": f"msg_{uuid.uuid4().hex[:24]}",
"type": "message",
"role": "assistant",
"content": [{
"type": "output_text",
"text": msg,
"annotations": [],
}],
"status": "completed",
}],
"usage": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
}
try:
return self.send_json(200, result)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
return
def _fb_retry_thinking_disabled(self, body, model, token, agent_id, stream, tracker, input_data, instructions, original_error, acct=None):
run_id = _codebuff_start_run(token, agent_id)
run_id, run_err = _codebuff_start_run(token, agent_id)
if not run_id:
return self.send_json(502, {"error": {"type": "upstream_error",
"message": "Failed to start codebuff agent run for retry."}})
msg = run_err[3] if run_err else "unknown error"
return self.send_json(run_err[1] if run_err else 502, {"error": {"type": run_err[0] if run_err else "upstream_error",
"message": f"Failed to start agent run for retry: {msg}"}})
instance_id = _codebuff_get_session(token, model)
messages = _fb_input_to_messages(input_data, instructions)
_codebuff_hard_disable_reasoning(messages)
@@ -5385,7 +5474,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"]
target = f"{_FREEBUFF_API_URL}/api/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.0", "x-codebuff-model": model}
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.7", "x-codebuff-model": model}
if instance_id:
headers["x-codebuff-instance-id"] = instance_id
print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)