v10.13.7: fix hash stripping, image bloat, thread safety, response logging

This commit is contained in:
Roman | RyzenAdvanced
2026-05-27 16:12:48 +04:00
Unverified
parent bc28f4bc60
commit da43297953
3 changed files with 71 additions and 41 deletions

View File

@@ -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 (<current_date> 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",
]),

View File

@@ -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 (<current_date> 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",
]),

View File

@@ -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'<current_date>[^<]*</current_date>', '', latest_norm)
latest_norm = re.sub(r'</?goal_context>', '', latest_norm)
latest_norm = re.sub(r'</?environment_context>', '', 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'<current_date>[^<]*</current_date>', '', latest_norm)
latest_norm = re.sub(r'</?goal_context>', '', latest_norm)
latest_norm = re.sub(r'</?environment_context>', '', 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):