From da432979530e149be291dc706250b641e608c01d Mon Sep 17 00:00:00 2001 From: Roman | RyzenAdvanced Date: Wed, 27 May 2026 16:12:48 +0400 Subject: [PATCH] v10.13.7: fix hash stripping, image bloat, thread safety, response logging --- codex-launcher-gui | 8 ++-- codex_launcher_lib.py | 7 +++- translate-proxy.py | 97 +++++++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/codex-launcher-gui b/codex-launcher-gui index aa68447..ae94ba0 100755 --- a/codex-launcher-gui +++ b/codex-launcher-gui @@ -27,7 +27,11 @@ model_catalog_json = "" """ CHANGELOG = [ - ("10.13.6", "2026-05-27", [ + ("10.13.7", "2026-05-27", [ + "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", @@ -36,8 +40,6 @@ CHANGELOG = [ "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", - "Fix compaction: max_input_items 60->200 for 1M-token models", - "Auto 401 token refresh on transient auth errors", "E2E test suite: bash test-antigravity.sh [--task]", "Merge cobra91 PR #17: MSIX Desktop launch, button state", ]), diff --git a/codex_launcher_lib.py b/codex_launcher_lib.py index fb446b0..327f8fc 100644 --- a/codex_launcher_lib.py +++ b/codex_launcher_lib.py @@ -83,7 +83,11 @@ model_catalog_json = "" """ CHANGELOG = [ - ("10.13.6", "2026-05-27", [ + ("10.13.7", "2026-05-27", [ + "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", @@ -92,7 +96,6 @@ CHANGELOG = [ "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", - "Fix compaction: max_input_items 60->200 for 1M-token models", "E2E test suite: bash test-antigravity.sh [--task]", "Merge cobra91 PR #17: MSIX Desktop launch, button state", ]), diff --git a/translate-proxy.py b/translate-proxy.py index e511ca8..c0fcf50 100755 --- a/translate-proxy.py +++ b/translate-proxy.py @@ -5103,7 +5103,24 @@ def _antigravity_normalize_context(input_data, model=""): for idx_t, t_item in enumerate(kept_tools): orig = t_item[1] out = orig.get("output", "") - if isinstance(out, str) and len(out) > _ANTIGRAVITY_MAX_TOOL_CHARS: + if isinstance(out, list): + cleaned = [] + for part in out: + if isinstance(part, dict) and part.get("type") in ("input_image", "image_url"): + url = part.get("image_url", {}).get("url", "") if isinstance(part.get("image_url"), dict) else "" + if url.startswith("data:"): + cleaned.append({"type": "text", "text": "[image data stripped for compaction]"}) + continue + cleaned.append(part) + if len(json.dumps(cleaned)) > _ANTIGRAVITY_MAX_TOOL_CHARS: + new_item = dict(orig) + new_item["output"] = json.dumps(cleaned)[:_ANTIGRAVITY_MAX_TOOL_CHARS] + "\n... [truncated]" + kept_tools[idx_t] = (t_item[0], new_item) + elif cleaned != out: + new_item = dict(orig) + new_item["output"] = cleaned + kept_tools[idx_t] = (t_item[0], new_item) + elif isinstance(out, str) and len(out) > _ANTIGRAVITY_MAX_TOOL_CHARS: new_item = dict(orig) new_item["output"] = out[:_ANTIGRAVITY_MAX_TOOL_CHARS] + f"\n... [truncated: kept {_ANTIGRAVITY_MAX_TOOL_CHARS} of {len(out)} chars]" kept_tools[idx_t] = (t_item[0], new_item) @@ -5800,10 +5817,12 @@ class Handler(http.server.BaseHTTPRequestHandler): latest_user = "\n".join(p.get("text", p.get("input_text", "")) for p in c if isinstance(p, dict)) break if latest_user: - latest_norm = " ".join(latest_user.strip().split())[:200] + latest_norm = " ".join(latest_user.strip().split())[:500] + latest_norm = re.sub(r'[^<]*', '', latest_norm) + latest_norm = re.sub(r'', '', latest_norm) + latest_norm = re.sub(r'', '', latest_norm) + latest_norm = " ".join(latest_norm.strip().split())[:200] latest_user_hash = hashlib.sha256(latest_norm.encode()).hexdigest()[:16] - - # Cross-session key: stable across retries for same task if latest_user_hash: task_key = _antigravity_loop_key(self._session_id, latest_user_hash) else: @@ -5868,9 +5887,10 @@ class Handler(http.server.BaseHTTPRequestHandler): f"STOP READING FILES AND APPLY YOUR EDITS NOW."}]}) # CHANGE 2: File-path read-loop detection - if ag_key not in _ANTIGRAVITY_FILE_TRACKER: - _ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0} - ft = _ANTIGRAVITY_FILE_TRACKER[ag_key] + with _ANTIGRAVITY_LOOP_TRACKER_LOCK: + if ag_key not in _ANTIGRAVITY_FILE_TRACKER: + _ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0} + ft = _ANTIGRAVITY_FILE_TRACKER[ag_key] for item in reversed(input_data): if isinstance(item, dict) and item.get("type") == "function_call": args_str = json.dumps(item.get("arguments", {})) @@ -6614,7 +6634,11 @@ class Handler(http.server.BaseHTTPRequestHandler): latest_user = "\n".join(p.get("text", p.get("input_text", "")) for p in c if isinstance(p, dict)) break if latest_user: - latest_norm = " ".join(latest_user.strip().split())[:200] + latest_norm = " ".join(latest_user.strip().split())[:500] + latest_norm = re.sub(r'[^<]*', '', latest_norm) + latest_norm = re.sub(r'', '', latest_norm) + latest_norm = re.sub(r'', '', latest_norm) + latest_norm = " ".join(latest_norm.strip().split())[:200] latest_user_hash = hashlib.sha256(latest_norm.encode()).hexdigest()[:16] if latest_user_hash: @@ -6677,9 +6701,10 @@ class Handler(http.server.BaseHTTPRequestHandler): f"{_ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK - cumulative_calls} remaining. " f"STOP READING AND WRITE NOW."}]}) - if ag_key not in _ANTIGRAVITY_FILE_TRACKER: - _ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0} - ft = _ANTIGRAVITY_FILE_TRACKER[ag_key] + with _ANTIGRAVITY_LOOP_TRACKER_LOCK: + if ag_key not in _ANTIGRAVITY_FILE_TRACKER: + _ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0} + ft = _ANTIGRAVITY_FILE_TRACKER[ag_key] for item in reversed(input_data): if isinstance(item, dict) and item.get("type") == "function_call": args_str = json.dumps(item.get("arguments", {})) @@ -8366,33 +8391,33 @@ class Handler(http.server.BaseHTTPRequestHandler): def _send_ag_finalize(self, text, stream=False, is_responses_api=True): sid = getattr(self, '_session_id', 'fin') print(f"[{sid}] [antigravity-finalize] Sending finalize response: {text[:80]}...", file=sys.stderr) + _log_resp(f"finalize-{sid}", "finalized", [{"type": "message", "content": [{"text": text}]}]) resp_id = f"resp_{uuid.uuid4().hex[:12]}" msg_id = f"msg_{uuid.uuid4().hex[:12]}" - if is_responses_api: - output_obj = [{"type": "message", "id": msg_id, "role": "assistant", - "content": [{"type": "output_text", "text": text}]}] - if stream: - events = [ - f"event: response.created\ndata: {json.dumps({'type':'response.created','response':{'id':resp_id,'object':'response','status':'in_progress'}})}\n\n", - f"event: response.output_item.added\ndata: {json.dumps({'type':'response.output_item.added','output_index':0,'item':{'type':'message','id':msg_id,'role':'assistant','content':[]}})}\n\n", - f"event: response.content_part.added\ndata: {json.dumps({'type':'response.content_part.added','output_index':0,'content_index':0,'part':{'type':'output_text','text':''}})}\n\n", - f"event: response.output_text.delta\ndata: {json.dumps({'type':'response.output_text.delta','output_index':0,'content_index':0,'delta':text})}\n\n", - f"event: response.output_text.done\ndata: {json.dumps({'type':'response.output_text.done','output_index':0,'content_index':0,'text':text})}\n\n", - f"event: response.content_part.done\ndata: {json.dumps({'type':'response.content_part.done','output_index':0,'content_index':0,'part':{'type':'output_text','text':text}})}\n\n", - f"event: response.output_item.done\ndata: {json.dumps({'type':'response.output_item.done','output_index':0,'item':{'type':'message','id':msg_id,'role':'assistant','content':[{'type':'output_text','text':text}]}})}\n\n", - f"event: response.completed\ndata: {json.dumps({'type':'response.completed','response':{'id':resp_id,'object':'response','status':'completed','output':output_obj}})}\n\n", - ] - self.send_response(200) - self.send_header("Content-Type", "text/event-stream") - self.send_header("Cache-Control", "no-cache") - self.send_header("Connection", "keep-alive") - self.end_headers() - for evt in events: - self.wfile.write(evt.encode()) - self.wfile.flush() - else: - self.send_json(200, {"id": resp_id, "object": "response", "status": "completed", - "output": output_obj, "model": "gemini-3-flash"}) + output_obj = [{"type": "message", "id": msg_id, "role": "assistant", + "content": [{"type": "output_text", "text": text}]}] + if stream: + events = [ + f"event: response.created\ndata: {json.dumps({'type':'response.created','response':{'id':resp_id,'object':'response','status':'in_progress'}})}\n\n", + f"event: response.output_item.added\ndata: {json.dumps({'type':'response.output_item.added','output_index':0,'item':{'type':'message','id':msg_id,'role':'assistant','content':[]}})}\n\n", + f"event: response.content_part.added\ndata: {json.dumps({'type':'response.content_part.added','output_index':0,'content_index':0,'part':{'type':'output_text','text':''}})}\n\n", + f"event: response.output_text.delta\ndata: {json.dumps({'type':'response.output_text.delta','output_index':0,'content_index':0,'delta':text})}\n\n", + f"event: response.output_text.done\ndata: {json.dumps({'type':'response.output_text.done','output_index':0,'content_index':0,'text':text})}\n\n", + f"event: response.content_part.done\ndata: {json.dumps({'type':'response.content_part.done','output_index':0,'content_index':0,'part':{'type':'output_text','text':text}})}\n\n", + f"event: response.output_item.done\ndata: {json.dumps({'type':'response.output_item.done','output_index':0,'item':{'type':'message','id':msg_id,'role':'assistant','content':[{'type':'output_text','text':text}]}})}\n\n", + f"event: response.completed\ndata: {json.dumps({'type':'response.completed','response':{'id':resp_id,'object':'response','status':'completed','output':output_obj}})}\n\n", + ] + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-cache") + self.send_header("Connection", "keep-alive") + self.end_headers() + for evt in events: + self.wfile.write(evt.encode()) + self.wfile.flush() + else: + self.send_json(200, {"id": resp_id, "object": "response", "status": "completed", + "output": output_obj, "model": "gemini-3-flash"}) return None def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):