v10.13.7: fix hash stripping, image bloat, thread safety, response logging
This commit is contained in:
@@ -27,7 +27,11 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection",
|
||||||
"Auto 401 token refresh with retry (v2 + OA compat)",
|
"Auto 401 token refresh with retry (v2 + OA compat)",
|
||||||
"Model-aware idle timeout: flash models 120s, pro 300s",
|
"Model-aware idle timeout: flash models 120s, pro 300s",
|
||||||
@@ -36,8 +40,6 @@ CHANGELOG = [
|
|||||||
"Anti-stall fix: no longer kills own parent/process group",
|
"Anti-stall fix: no longer kills own parent/process group",
|
||||||
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
||||||
"Fix Codex CLI 0.134.0 profiles: separate .config.toml files",
|
"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]",
|
"E2E test suite: bash test-antigravity.sh [--task]",
|
||||||
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection",
|
||||||
"Auto 401 token refresh with retry",
|
"Auto 401 token refresh with retry",
|
||||||
"Model-aware idle timeout: flash 120s, pro 300s",
|
"Model-aware idle timeout: flash 120s, pro 300s",
|
||||||
@@ -92,7 +96,6 @@ CHANGELOG = [
|
|||||||
"Anti-stall fix: no longer kills own parent/process group",
|
"Anti-stall fix: no longer kills own parent/process group",
|
||||||
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
||||||
"Fix Codex CLI 0.134.0 profiles: separate .config.toml files",
|
"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]",
|
"E2E test suite: bash test-antigravity.sh [--task]",
|
||||||
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -5103,7 +5103,24 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
for idx_t, t_item in enumerate(kept_tools):
|
for idx_t, t_item in enumerate(kept_tools):
|
||||||
orig = t_item[1]
|
orig = t_item[1]
|
||||||
out = orig.get("output", "")
|
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 = dict(orig)
|
||||||
new_item["output"] = out[:_ANTIGRAVITY_MAX_TOOL_CHARS] + f"\n... [truncated: kept {_ANTIGRAVITY_MAX_TOOL_CHARS} of {len(out)} chars]"
|
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)
|
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))
|
latest_user = "\n".join(p.get("text", p.get("input_text", "")) for p in c if isinstance(p, dict))
|
||||||
break
|
break
|
||||||
if latest_user:
|
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]
|
latest_user_hash = hashlib.sha256(latest_norm.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
# Cross-session key: stable across retries for same task
|
|
||||||
if latest_user_hash:
|
if latest_user_hash:
|
||||||
task_key = _antigravity_loop_key(self._session_id, latest_user_hash)
|
task_key = _antigravity_loop_key(self._session_id, latest_user_hash)
|
||||||
else:
|
else:
|
||||||
@@ -5868,9 +5887,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
f"STOP READING FILES AND APPLY YOUR EDITS NOW."}]})
|
f"STOP READING FILES AND APPLY YOUR EDITS NOW."}]})
|
||||||
|
|
||||||
# CHANGE 2: File-path read-loop detection
|
# CHANGE 2: File-path read-loop detection
|
||||||
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
with _ANTIGRAVITY_LOOP_TRACKER_LOCK:
|
||||||
_ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0}
|
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
||||||
ft = _ANTIGRAVITY_FILE_TRACKER[ag_key]
|
_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):
|
for item in reversed(input_data):
|
||||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||||
args_str = json.dumps(item.get("arguments", {}))
|
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))
|
latest_user = "\n".join(p.get("text", p.get("input_text", "")) for p in c if isinstance(p, dict))
|
||||||
break
|
break
|
||||||
if latest_user:
|
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]
|
latest_user_hash = hashlib.sha256(latest_norm.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
if latest_user_hash:
|
if latest_user_hash:
|
||||||
@@ -6677,9 +6701,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
f"{_ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK - cumulative_calls} remaining. "
|
f"{_ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK - cumulative_calls} remaining. "
|
||||||
f"STOP READING AND WRITE NOW."}]})
|
f"STOP READING AND WRITE NOW."}]})
|
||||||
|
|
||||||
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
with _ANTIGRAVITY_LOOP_TRACKER_LOCK:
|
||||||
_ANTIGRAVITY_FILE_TRACKER[ag_key] = {"last_path": None, "path_counts": {}, "total_reads": 0}
|
if ag_key not in _ANTIGRAVITY_FILE_TRACKER:
|
||||||
ft = _ANTIGRAVITY_FILE_TRACKER[ag_key]
|
_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):
|
for item in reversed(input_data):
|
||||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||||
args_str = json.dumps(item.get("arguments", {}))
|
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):
|
def _send_ag_finalize(self, text, stream=False, is_responses_api=True):
|
||||||
sid = getattr(self, '_session_id', 'fin')
|
sid = getattr(self, '_session_id', 'fin')
|
||||||
print(f"[{sid}] [antigravity-finalize] Sending finalize response: {text[:80]}...", file=sys.stderr)
|
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]}"
|
resp_id = f"resp_{uuid.uuid4().hex[:12]}"
|
||||||
msg_id = f"msg_{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",
|
||||||
output_obj = [{"type": "message", "id": msg_id, "role": "assistant",
|
"content": [{"type": "output_text", "text": text}]}]
|
||||||
"content": [{"type": "output_text", "text": text}]}]
|
if stream:
|
||||||
if stream:
|
events = [
|
||||||
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.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.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.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.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.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.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.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",
|
||||||
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_response(200)
|
self.send_header("Content-Type", "text/event-stream")
|
||||||
self.send_header("Content-Type", "text/event-stream")
|
self.send_header("Cache-Control", "no-cache")
|
||||||
self.send_header("Cache-Control", "no-cache")
|
self.send_header("Connection", "keep-alive")
|
||||||
self.send_header("Connection", "keep-alive")
|
self.end_headers()
|
||||||
self.end_headers()
|
for evt in events:
|
||||||
for evt in events:
|
self.wfile.write(evt.encode())
|
||||||
self.wfile.write(evt.encode())
|
self.wfile.flush()
|
||||||
self.wfile.flush()
|
else:
|
||||||
else:
|
self.send_json(200, {"id": resp_id, "object": "response", "status": "completed",
|
||||||
self.send_json(200, {"id": resp_id, "object": "response", "status": "completed",
|
"output": output_obj, "model": "gemini-3-flash"})
|
||||||
"output": output_obj, "model": "gemini-3-flash"})
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):
|
def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user