Compare commits
4 Commits
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v3.11.11 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Fix: Stricter function_call/output Pairing + Gemini Sanitizer Rewrite (PR #12)**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Stricter function_call/output pairing**: Only includes pairs where BOTH call and output exist — no orphan calls sent to Gemini
|
||||||
|
- **Gemini sanitizer rewritten**: Tool messages (`functionCall`/`functionResponse`) are always preserved as-is, never merged or skipped
|
||||||
|
- **Text merging more conservative**: Checks last message for tool content before merging consecutive text messages
|
||||||
|
- **Final trimming safe**: Only removes plain `message` items, never `function_call_output` (which would break tool pairs)
|
||||||
|
- **Merge PR #12**: Fix by qwen-chat coder
|
||||||
|
|
||||||
|
## v3.11.10 (2026-05-26)
|
||||||
|
|
||||||
|
## v3.11.10 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Fix: Interleave function_call/output Pairs, Gemini Turn Trimming (PR #11)**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Fix Antigravity function_call/output ordering**: Tool calls and their responses are now properly interleaved in sequence (`function_call` → `function_call_output` → `function_call` → ...) instead of being grouped separately
|
||||||
|
- **Gemini sanitizer trimming**: Leading/trailing non-user turns removed for Google API compliance (Google requires conversation to start and end with user turn)
|
||||||
|
- **Stricter role boundary enforcement**: `functionCall` (model) and `functionResponse` (user) never merged across role boundaries
|
||||||
|
- **Merge PR #11**: Fix by qwen-chat coder
|
||||||
|
|
||||||
|
## v3.11.9 (2026-05-26)
|
||||||
|
|
||||||
|
## v3.11.9 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Fix: Preserve functionCall/functionResponse in Gemini Sanitizer (PR #10)**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Fix Antigravity multi-turn tool use**: The Gemini message sanitizer was incorrectly merging/dropping `functionCall` and `functionResponse` turns, causing Antigravity to think forever without responding. These turns are now always preserved as separate messages.
|
||||||
|
- **Merge PR #10**: `fix: preserve functionCall/functionResponse in Gemini sanitizer` (qwen-chat coder)
|
||||||
|
|
||||||
|
## v3.11.8 (2026-05-26)
|
||||||
|
|
||||||
## v3.11.8 (2026-05-26)
|
## v3.11.8 (2026-05-26)
|
||||||
|
|
||||||
**Vision Cache Persistence, PR #8 Merge**
|
**Vision Cache Persistence, PR #8 Merge**
|
||||||
|
|||||||
BIN
codex-launcher_3.11.11_all.deb
Normal file
BIN
codex-launcher_3.11.11_all.deb
Normal file
Binary file not shown.
Binary file not shown.
@@ -27,6 +27,18 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
("3.11.11", "2026-05-26", [
|
||||||
|
"Fix Antigravity: stricter function_call/output pairing (PR #12)",
|
||||||
|
"Gemini sanitizer rewritten — tool messages always preserved",
|
||||||
|
]),
|
||||||
|
("3.11.10", "2026-05-26", [
|
||||||
|
"Fix Antigravity: interleave function_call/output pairs (PR #11)",
|
||||||
|
"Gemini sanitizer: trim non-user turns for Google API compliance",
|
||||||
|
]),
|
||||||
|
("3.11.9", "2026-05-26", [
|
||||||
|
"Fix Antigravity: preserve functionCall/functionResponse (PR #10)",
|
||||||
|
"Prevents tool responses from being dropped in multi-turn sessions",
|
||||||
|
]),
|
||||||
("3.11.8", "2026-05-26", [
|
("3.11.8", "2026-05-26", [
|
||||||
"Vision cache persisted across requests (PR #8 merge)",
|
"Vision cache persisted across requests (PR #8 merge)",
|
||||||
"No redundant vision API calls for same image URL",
|
"No redundant vision API calls for same image URL",
|
||||||
|
|||||||
@@ -83,6 +83,21 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
("3.11.11", "2026-05-26", [
|
||||||
|
"Fix Antigravity: stricter function_call/output pairing + Gemini sanitizer rewrite (PR #12)",
|
||||||
|
"Only pairs where BOTH call and output exist are included — no orphan calls",
|
||||||
|
"Gemini sanitizer: tool messages always preserved as-is, text merging more conservative",
|
||||||
|
"Final trimming only removes plain messages, never function_call_output",
|
||||||
|
]),
|
||||||
|
("3.11.10", "2026-05-26", [
|
||||||
|
"Fix Antigravity: interleave function_call/output pairs in correct sequence (PR #11)",
|
||||||
|
"Fix Gemini sanitizer: trim leading/trailing non-user turns for Google API compliance",
|
||||||
|
"Stricter function call/response isolation — no merging across role boundaries",
|
||||||
|
]),
|
||||||
|
("3.11.9", "2026-05-26", [
|
||||||
|
"Fix Antigravity: preserve functionCall/functionResponse in Gemini sanitizer (PR #10)",
|
||||||
|
"Prevents tool responses from being merged/dropped in multi-turn Antigravity sessions",
|
||||||
|
]),
|
||||||
("3.11.8", "2026-05-26", [
|
("3.11.8", "2026-05-26", [
|
||||||
"Vision description cache persisted across requests (no redundant API calls for same image)",
|
"Vision description cache persisted across requests (no redundant API calls for same image)",
|
||||||
"Merge PR #8: fix vision cache persistence across requests",
|
"Merge PR #8: fix vision cache persistence across requests",
|
||||||
|
|||||||
@@ -4841,6 +4841,14 @@ def _antigravity_is_simple_user(text):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _antigravity_normalize_context(input_data, model=""):
|
def _antigravity_normalize_context(input_data, model=""):
|
||||||
|
"""
|
||||||
|
Normalize context for Antigravity while PRESERVING function_call -> function_call_output pairs.
|
||||||
|
|
||||||
|
Google's Gemini API requires STRICT alternation:
|
||||||
|
- functionCall (role=model) MUST be immediately followed by functionResponse (role=user)
|
||||||
|
|
||||||
|
This function compacts old history but NEVER breaks tool call/response pairs.
|
||||||
|
"""
|
||||||
if not isinstance(input_data, list) or len(input_data) < 2:
|
if not isinstance(input_data, list) or len(input_data) < 2:
|
||||||
return input_data
|
return input_data
|
||||||
is_claude_model = "claude" in model.lower()
|
is_claude_model = "claude" in model.lower()
|
||||||
@@ -4889,7 +4897,7 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
dev_messages = []
|
dev_messages = []
|
||||||
recent_items = []
|
recent_items = []
|
||||||
tool_outputs = []
|
tool_outputs = []
|
||||||
other_items = []
|
tool_calls = []
|
||||||
|
|
||||||
for i, item in enumerate(input_data):
|
for i, item in enumerate(input_data):
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
@@ -4899,8 +4907,8 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
dev_messages.append(item)
|
dev_messages.append(item)
|
||||||
elif t == "function_call_output":
|
elif t == "function_call_output":
|
||||||
tool_outputs.append((i, item))
|
tool_outputs.append((i, item))
|
||||||
elif t in ("function_call",):
|
elif t == "function_call":
|
||||||
other_items.append((i, item))
|
tool_calls.append((i, item))
|
||||||
elif t == "message":
|
elif t == "message":
|
||||||
recent_items.append((i, item))
|
recent_items.append((i, item))
|
||||||
|
|
||||||
@@ -4946,18 +4954,14 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
deduped_tail.append((idx, msg_item))
|
deduped_tail.append((idx, msg_item))
|
||||||
recent_tail = deduped_tail if deduped_tail else recent_tail
|
recent_tail = deduped_tail if deduped_tail else recent_tail
|
||||||
|
|
||||||
tool_call_ids = set()
|
# Build call_id -> function_call mapping
|
||||||
for _, t_item in kept_tools:
|
tool_call_map = {}
|
||||||
cid = t_item.get("call_id", t_item.get("id", ""))
|
for _, call_item in tool_calls:
|
||||||
|
cid = call_item.get("call_id", call_item.get("id", ""))
|
||||||
if cid:
|
if cid:
|
||||||
tool_call_ids.add(cid)
|
tool_call_map[cid] = call_item
|
||||||
|
|
||||||
paired_calls = []
|
|
||||||
for idx, item in other_items:
|
|
||||||
cid = item.get("call_id", item.get("id", ""))
|
|
||||||
if cid in tool_call_ids:
|
|
||||||
paired_calls.append((idx, item))
|
|
||||||
|
|
||||||
|
# Build result: maintain PAIRED sequence (function_call -> function_call_output)
|
||||||
result = list(dev_messages)
|
result = list(dev_messages)
|
||||||
|
|
||||||
compaction_summaries = []
|
compaction_summaries = []
|
||||||
@@ -4973,11 +4977,22 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
summary_text = f"[Tool history summary: {n_summarized} older tool outputs omitted. {n_tool_calls} prior function calls were made for file inspection/editing.]"
|
summary_text = f"[Tool history summary: {n_summarized} older tool outputs omitted. {n_tool_calls} prior function calls were made for file inspection/editing.]"
|
||||||
result.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": summary_text}]})
|
result.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": summary_text}]})
|
||||||
|
|
||||||
for _, call_item in paired_calls:
|
# CRITICAL: Add tool CALLS and their corresponding OUTPUTS in PAIRED ORDER
|
||||||
result.append(call_item)
|
# Only include pairs where BOTH call and output are present
|
||||||
|
added_pairs = set()
|
||||||
for _, tool_item in kept_tools:
|
for _, tool_item in kept_tools:
|
||||||
result.append(tool_item)
|
cid = tool_item.get("call_id", tool_item.get("id", ""))
|
||||||
|
if cid and cid in tool_call_map and cid not in added_pairs:
|
||||||
|
# Add function_call FIRST, then function_call_output IMMEDIATELY
|
||||||
|
result.append(tool_call_map[cid])
|
||||||
|
result.append(tool_item)
|
||||||
|
added_pairs.add(cid)
|
||||||
|
|
||||||
|
# Add any orphan tool outputs (no matching call found) - these go at the end before messages
|
||||||
|
for _, tool_item in kept_tools:
|
||||||
|
cid = tool_item.get("call_id", tool_item.get("id", ""))
|
||||||
|
if cid not in added_pairs:
|
||||||
|
result.append(tool_item)
|
||||||
|
|
||||||
for cs_item in compaction_summaries:
|
for cs_item in compaction_summaries:
|
||||||
result.append(cs_item)
|
result.append(cs_item)
|
||||||
@@ -5017,7 +5032,7 @@ def _antigravity_normalize_context(input_data, model=""):
|
|||||||
|
|
||||||
while len(result) > _ANTIGRAVITY_MAX_CONTENTS and total_chars > _ANTIGRAVITY_SOFT_CHARS:
|
while len(result) > _ANTIGRAVITY_MAX_CONTENTS and total_chars > _ANTIGRAVITY_SOFT_CHARS:
|
||||||
for i in range(1, len(result) - 1):
|
for i in range(1, len(result) - 1):
|
||||||
if isinstance(result[i], dict) and result[i].get("type") in ("message", "function_call_output"):
|
if isinstance(result[i], dict) and result[i].get("type") in ("message",):
|
||||||
removed = result.pop(i)
|
removed = result.pop(i)
|
||||||
total_chars -= len(json.dumps(removed, ensure_ascii=False))
|
total_chars -= len(json.dumps(removed, ensure_ascii=False))
|
||||||
break
|
break
|
||||||
@@ -5553,6 +5568,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
resp_part["functionResponse"]["id"] = call_id
|
resp_part["functionResponse"]["id"] = call_id
|
||||||
contents.append({"role": "user", "parts": [resp_part]})
|
contents.append({"role": "user", "parts": [resp_part]})
|
||||||
|
|
||||||
|
# CRITICAL FIX: Sanitize contents while PRESERVING functionCall -> functionResponse alternation.
|
||||||
|
# Google's Gemini API REQUIRES: functionCall (role=model) must be immediately followed by functionResponse (role=user).
|
||||||
|
# We NEVER merge, skip, or reorder tool-related messages.
|
||||||
if OAUTH_PROVIDER.startswith("google") and "claude" not in model.lower():
|
if OAUTH_PROVIDER.startswith("google") and "claude" not in model.lower():
|
||||||
sanitized = []
|
sanitized = []
|
||||||
last_user_text = None
|
last_user_text = None
|
||||||
@@ -5562,18 +5580,40 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
parts = [p for p in content.get("parts", []) if isinstance(p, dict)]
|
parts = [p for p in content.get("parts", []) if isinstance(p, dict)]
|
||||||
if not parts:
|
if not parts:
|
||||||
continue
|
continue
|
||||||
|
# Check if this content has functionCall or functionResponse - these MUST be preserved as-is
|
||||||
|
has_function_call = any("functionCall" in p for p in parts)
|
||||||
|
has_function_response = any("functionResponse" in p for p in parts)
|
||||||
text_key = "\n".join([p.get("text", "") for p in parts if "text" in p]).strip()
|
text_key = "\n".join([p.get("text", "") for p in parts if "text" in p]).strip()
|
||||||
|
|
||||||
|
# Tool calls/responses are NEVER merged or skipped - they must maintain strict order
|
||||||
|
if has_function_call or has_function_response:
|
||||||
|
sanitized.append({"role": role, "parts": parts})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# For plain text messages only: skip duplicate consecutive user text
|
||||||
if role == "user" and text_key and text_key == last_user_text:
|
if role == "user" and text_key and text_key == last_user_text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Merge consecutive same-role TEXT-ONLY messages (no tool content)
|
||||||
if role == last_role and role in ("user", "model") and sanitized:
|
if role == last_role and role in ("user", "model") and sanitized:
|
||||||
sanitized[-1].setdefault("parts", []).extend(parts)
|
last_parts = sanitized[-1].get("parts", [])
|
||||||
else:
|
# Only merge if the last message is also text-only (no functionCall/functionResponse)
|
||||||
sanitized.append({"role": role, "parts": parts})
|
last_has_tool = any("functionCall" in p or "functionResponse" in p for p in last_parts)
|
||||||
|
if not last_has_tool:
|
||||||
|
sanitized[-1].setdefault("parts", []).extend(parts)
|
||||||
|
if role == "user" and text_key:
|
||||||
|
last_user_text = text_key
|
||||||
|
continue
|
||||||
|
|
||||||
|
sanitized.append({"role": role, "parts": parts})
|
||||||
if role == "user" and text_key:
|
if role == "user" and text_key:
|
||||||
last_user_text = text_key
|
last_user_text = text_key
|
||||||
last_role = role
|
last_role = role
|
||||||
|
|
||||||
|
# Trim leading non-user messages (Google expects conversation to start with user)
|
||||||
while sanitized and sanitized[0].get("role") != "user":
|
while sanitized and sanitized[0].get("role") != "user":
|
||||||
sanitized.pop(0)
|
sanitized.pop(0)
|
||||||
|
# Trim trailing non-user messages (must end with user turn for continuation)
|
||||||
while sanitized and sanitized[-1].get("role") != "user":
|
while sanitized and sanitized[-1].get("role") != "user":
|
||||||
sanitized.pop()
|
sanitized.pop()
|
||||||
contents = sanitized
|
contents = sanitized
|
||||||
|
|||||||
Reference in New Issue
Block a user