From 6861700c0d24fda54075a3c11fefeb72043dd3b4 Mon Sep 17 00:00:00 2001 From: Roman | RyzenAdvanced Date: Wed, 27 May 2026 17:08:21 +0400 Subject: [PATCH] v10.13.8: hard tool-strip on budget cap, compaction output stripping, null-tool loop detection --- codex-launcher-gui | 21 ++++------ codex_launcher_lib.py | 21 ++++------ translate-proxy.py | 94 ++++++++++++++++++++++++++++++------------- 3 files changed, 81 insertions(+), 55 deletions(-) diff --git a/codex-launcher-gui b/codex-launcher-gui index ae94ba0..e9f510c 100755 --- a/codex-launcher-gui +++ b/codex-launcher-gui @@ -27,21 +27,14 @@ model_catalog_json = "" """ CHANGELOG = [ - ("10.13.7", "2026-05-27", [ + ("10.13.8", "2026-05-27", [ + "Fix: compaction summary strips raw tool outputs (was re-triggering read loops)", + "Fix: budget cap now strips tools from request (model literally cannot call tools)", + "Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)", + "Fix: post-compaction write directive when 10+ reads with 0 writes", "Fix: strip timestamps from loop hash ( broke cross-session tracker)", - "Fix: strip base64 image data from tool outputs in normalizer (multi-MB payload bloat)", - "Fix: thread-safe file tracker (was unprotected dict in ThreadingHTTPServer)", - "Fix: response logging for finalize and budget-cap paths", - "Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection", - "Auto 401 token refresh with retry (v2 + OA compat)", - "Model-aware idle timeout: flash models 120s, pro 300s", - "Smart compaction: directive summary on read-loops", - "Default provider policy for unrecognized backends", - "Anti-stall fix: no longer kills own parent/process group", - "Codex Desktop Updater: check/install/rollback/manual rebuild", - "Fix Codex CLI 0.134.0 profiles: separate .config.toml files", - "E2E test suite: bash test-antigravity.sh [--task]", - "Merge cobra91 PR #17: MSIX Desktop launch, button state", + "Fix: strip base64 image data from tool outputs in normalizer", + "Fix: thread-safe file tracker, response logging for finalize/budget paths", ]), ("3.12.1", "2026-05-27", [ "Fix Antigravity adapter (PR #15): simplified model resolution", diff --git a/codex_launcher_lib.py b/codex_launcher_lib.py index 327f8fc..0ac67f2 100644 --- a/codex_launcher_lib.py +++ b/codex_launcher_lib.py @@ -83,21 +83,14 @@ model_catalog_json = "" """ CHANGELOG = [ - ("10.13.7", "2026-05-27", [ + ("10.13.8", "2026-05-27", [ + "Fix: compaction summary strips raw tool outputs (was re-triggering read loops)", + "Fix: budget cap now strips tools from request (model literally cannot call tools)", + "Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)", + "Fix: post-compaction write directive when 10+ reads with 0 writes", "Fix: strip timestamps from loop hash ( broke cross-session tracker)", - "Fix: strip base64 image data from tool outputs in normalizer (multi-MB payload bloat)", - "Fix: thread-safe file tracker (was unprotected dict in ThreadingHTTPServer)", - "Fix: response logging for finalize and budget-cap paths", - "Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection", - "Auto 401 token refresh with retry", - "Model-aware idle timeout: flash 120s, pro 300s", - "Smart compaction: directive summary on read-loops", - "Default provider policy for unrecognized backends", - "Anti-stall fix: no longer kills own parent/process group", - "Codex Desktop Updater: check/install/rollback/manual rebuild", - "Fix Codex CLI 0.134.0 profiles: separate .config.toml files", - "E2E test suite: bash test-antigravity.sh [--task]", - "Merge cobra91 PR #17: MSIX Desktop launch, button state", + "Fix: strip base64 image data from tool outputs in normalizer", + "Fix: thread-safe file tracker, response logging for finalize/budget paths", ]), ("3.12.1", "2026-05-27", [ "Fix Antigravity adapter (PR #15): simplify model resolution", diff --git a/translate-proxy.py b/translate-proxy.py index c0fcf50..7acf16d 100755 --- a/translate-proxy.py +++ b/translate-proxy.py @@ -1879,7 +1879,7 @@ def _crof_compact_for_retry(input_data, model, aggression=0): return head + tail summary_lines = [f"[Auto-compacted: {len(body)} turns removed (adaptive limit={limit})]"] - for item in body[-5:]: + for item in body[-3:]: summary_lines.append(_item_summary(item, max_len=120)) summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]} @@ -1908,9 +1908,13 @@ def _item_summary(item, max_len=200): return f"[tool call] {name}({args[:max_len]})" elif t == "function_call_output": output = item.get("output", "") - if len(output) > max_len: - return f"[tool result] {output[:max_len]}..." - return f"[tool result] {output}" + out_len = len(output) if isinstance(output, str) else len(str(output)) + exit_match = re.search(r'Process exited with code (\d+)', output if isinstance(output, str) else str(output)) + exit_code = exit_match.group(1) if exit_match else "?" + tok_match = re.search(r'Original token count: (\d+)', output if isinstance(output, str) else str(output)) + tok_count = tok_match.group(1) if tok_match else "" + tok_info = f" ({tok_count} tokens)" if tok_count else "" + return f"[tool result] exit={exit_code}{tok_info} {out_len} chars" return f"[{t}]" def _extract_files(items): @@ -1998,6 +2002,10 @@ def _compact_input(input_data): tool_summaries.append(_item_summary(item, max_len=150)) files = _extract_files(body) + n_read_tools = sum(1 for it in body if it.get("type") == "function_call" + and any(k in it.get("arguments", "") for k in ["cat ", "head ", "tail ", "sed -n", "grep ", "less ", "more ", "python3 -c", ".read()"])) + n_write_tools = sum(1 for it in body if it.get("type") == "function_call" + and any(k in it.get("arguments", "") for k in ["write(", ".write", " > ", " >> ", "sed -i", "patch "])) summary_lines = [f"[Auto-compacted: {len(body)} earlier turns summarized to preserve context]"] if user_queries: @@ -2005,11 +2013,13 @@ def _compact_input(input_data): if assistant_msgs: summary_lines.append(f"Assistant responses: {'; '.join(assistant_msgs[-3:])}") if tool_summaries: - summary_lines.append(f"Actions taken ({len(tool_summaries)} steps):") - for ts in tool_summaries[-15:]: + summary_lines.append(f"Actions taken ({len(tool_summaries)} steps, {n_read_tools} reads, {n_write_tools} writes):") + for ts in tool_summaries[-5:]: summary_lines.append(f" {ts}") if files: summary_lines.append(f"Files touched: {', '.join(sorted(files)[-10:])}") + if n_read_tools > 10 and n_write_tools == 0: + summary_lines.append("⚠ You have already read these files extensively but made NO edits. You MUST write your changes NOW. Do NOT read any more files.") summary_text = "\n".join(summary_lines) summary_msg = { @@ -2135,7 +2145,7 @@ def _adaptive_compact(input_data, model, policy=None): if not body: return head + tail, True summary_lines = [f"[Auto-compacted: {len(body)} turns removed (budget={input_budget}tok, model={model})]"] - for item in body[-5:]: + for item in body[-3:]: summary_lines.append(_item_summary(item, max_len=120)) summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]} @@ -5907,6 +5917,18 @@ class Handler(http.server.BaseHTTPRequestHandler): file=sys.stderr) break + null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"} + consecutive_null = 0 + for item in reversed(input_data): + if isinstance(item, dict): + if item.get("type") == "function_call" and item.get("name") in null_tool_names: + consecutive_null += 1 + elif item.get("type") == "function_call": + break + if consecutive_null >= 3: + ag_state["force_finalize"] = True + print(f"[{getattr(self, '_session_id', '?')}] [antigravity-loop] NULL-TOOL LOOP: {consecutive_null} consecutive {null_tool_names} calls, forcing finalize", file=sys.stderr) + last_tool_key = None for item in reversed(input_data): if isinstance(item, dict) and item.get("type") == "function_call": @@ -5940,9 +5962,12 @@ class Handler(http.server.BaseHTTPRequestHandler): request_body["systemInstruction"] = {"role": "user", "parts": system_parts} if gen_config: request_body["generationConfig"] = gen_config - if gemini_tools: + _budget_exceeded = ag_state.get("total_tool_calls", 0) > _ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK + if gemini_tools and not _budget_exceeded and not ag_state.get("force_finalize"): request_body["tools"] = gemini_tools - if _is_claude_model and gemini_tools: + elif _budget_exceeded or ag_state.get("force_finalize"): + print(f"[{getattr(self, '_session_id', '?')}] [antigravity-budget] TOOLS STRIPPED from request (budget exceeded or force_finalize)", file=sys.stderr) + if _is_claude_model and "tools" in request_body: request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}} import platform as _plat @@ -6720,22 +6745,34 @@ class Handler(http.server.BaseHTTPRequestHandler): f"{ft['path_counts'][dp]}x, total={ft['total_reads']}", file=sys.stderr) break - last_tool_key = None - for item in reversed(input_data): - if isinstance(item, dict) and item.get("type") == "function_call": - fname = item.get("name", "") - args_str = json.dumps(item.get("arguments", {}), sort_keys=True)[:100] - last_tool_key = f"{fname}:{args_str}" - break - if last_tool_key: - if last_tool_key == ag_state["last_tool"]: - ag_state["last_tool_count"] += 1 - if ag_state["last_tool_count"] >= 5: - ag_state["repeated_tool"] = True - ag_state["force_finalize"] = True - else: - ag_state["last_tool"] = last_tool_key - ag_state["last_tool_count"] = 1 + null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"} + consecutive_null = 0 + for item in reversed(input_data): + if isinstance(item, dict): + if item.get("type") == "function_call" and item.get("name") in null_tool_names: + consecutive_null += 1 + elif item.get("type") == "function_call": + break + if consecutive_null >= 3: + ag_state["force_finalize"] = True + print(f"[{getattr(self, '_session_id', '?')}] [antigravity-loop] NULL-TOOL LOOP: {consecutive_null} consecutive {null_tool_names} calls, forcing finalize", file=sys.stderr) + + last_tool_key = None + for item in reversed(input_data): + if isinstance(item, dict) and item.get("type") == "function_call": + fname = item.get("name", "") + args_str = json.dumps(item.get("arguments", {}), sort_keys=True)[:100] + last_tool_key = f"{fname}:{args_str}" + break + if last_tool_key: + if last_tool_key == ag_state["last_tool"]: + ag_state["last_tool_count"] += 1 + if ag_state["last_tool_count"] >= 5: + ag_state["repeated_tool"] = True + ag_state["force_finalize"] = True + else: + ag_state["last_tool"] = last_tool_key + ag_state["last_tool_count"] = 1 _EDIT_WORDS = ("change", "fix", "update", "redesign", "rewrite", "modify", "improve", "replace", "edit", "make it", "add", "remove", "delete", "rename", "move", "convert") latest_lower = "" @@ -6789,10 +6826,13 @@ class Handler(http.server.BaseHTTPRequestHandler): request_body["systemInstruction"] = {"parts": system_parts} if gen_config: request_body["generationConfig"] = gen_config - if gemini_tools: + _budget_exceeded_oa = ag_state.get("total_tool_calls", 0) > _ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK + if gemini_tools and not _budget_exceeded_oa and not ag_state.get("force_finalize"): request_body["tools"] = gemini_tools + elif _budget_exceeded_oa or ag_state.get("force_finalize"): + print(f"[antigravity-budget] TOOLS STRIPPED from OA request (budget exceeded or force_finalize)", file=sys.stderr) - if OAUTH_PROVIDER == "google-antigravity" and _is_claude_model and gemini_tools: + if OAUTH_PROVIDER == "google-antigravity" and _is_claude_model and "tools" in request_body: request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}} if _is_claude_thinking: print(f"[antigravity-claude] applied VALIDATED toolConfig for thinking model", file=sys.stderr)