diff --git a/codex-launcher_3.6.0_all.deb b/codex-launcher_3.6.0_all.deb index 08b937b..f8f5332 100644 Binary files a/codex-launcher_3.6.0_all.deb and b/codex-launcher_3.6.0_all.deb differ diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 7f6a65f..ab635af 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -83,6 +83,21 @@ FIX 8: Adaptive probing caused format mismatch (REVERTED) - ErrorAnalyzer learning on retries (not proactive probes) Location: Reverted to cc_input_to_messages(), removed _build_cc_messages + _probe_cc_format +FIX 21: DSML parser silently drops tool calls when model uses name="cmd" (THE HALT BUG) + Symptom: Codex CLI stops mid-task. Model generates valid DSML exec_command with + <||DSML||parameter name="cmd" string="true">curl ... + Parser returns parsed_tool_calls=0. Client sees text output but no tool to execute. + CLI has nothing to do and halts. + Root cause: Line 1798 had `if key == "command":` — only matching parameter name="command". + The actual tool schema defines the parameter as "cmd" (see exec_command schema). + When DeepSeek generates name="cmd", the key "cmd" != "command", so cmd stays None, + and line 1825-1826 `if not cmd: continue` silently skips the entire tool call. + The XML parser (line 2205) already handled both: `params.get("command") or params.get("cmd")` + but the DSML parser did not. + Fix: Changed to `if key in ("command", "cmd"):` in the DSML parameter loop. + Test: Pattern L self-test verifies DSML with name="cmd" is parsed correctly. + Location: _parse_commandcode_text_tool_calls() DSML parameter loop, self-test Pattern L + ═══════════════════════════════════════════════════════════════════ """ @@ -204,6 +219,7 @@ _pool = uuid.uuid4().hex[:8] _antigravity_version = "1.18.3" _antigravity_version_checked = 0 _antigravity_version_lock = threading.Lock() +_last_user_urls = collections.deque(maxlen=20) _conn_pool_lock = threading.Lock() _conn_pool = {} @@ -1739,6 +1755,39 @@ def _parse_commandcode_text_tool_calls(text): calls = [] if not text: return calls + + # [FIX 20] Use the module-level _build_explore_cmd helper (moved to module level + # in FIX 22 so it can also be called from cc_stream_to_sse for escalation recovery). + # The function is defined above, before _parse_commandcode_text_tool_calls. + def _build_explore_cmd_local(text_for_url): + if not text_for_url: + return None, None + url_m = re.search(r"https?://[^\s\]'\\>\"]+", text_for_url) + repo_url = url_m.group(0).rstrip(")].,;'\\") if url_m else "" + if repo_url: + # Clean trailing .git if present + if repo_url.endswith(".git"): + repo_url = repo_url[:-4] + if "/api/v1/repos/" not in repo_url: + host_m = re.match(r"(https?://[^/]+)/(.*)", repo_url) + if host_m: + host, path = host_m.groups() + api_base = f"{host}/api/v1/repos/{path}" + else: + api_base = repo_url.replace("/admin/", "/api/v1/repos/") + else: + api_base = repo_url + + cmd = ( + f"cd /tmp && " + f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | " + f"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); print(base64.b64decode(d['content']).decode())\" 2>/dev/null | head -600 && " + f"curl -sL --max-time 15 '{api_base}/contents' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print('\\n'.join(f'{{x.get(\'path\')}} {{x.get(\'type\')}}' for x in d[:50]))\" 2>/dev/null && " + f"curl -sL --max-time 15 '{api_base}/releases' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(json.dumps(d[:3], indent=2)[:2000])\" 2>/dev/null" + ) + return cmd, "Explore repository to understand the app and gather README, root contents, and releases for the landing page." + return None, None + # [FIX 17] DSML tool_call blocks used by the model now. # Example: # <||DSML||tool_calls> @@ -1763,7 +1812,12 @@ def _parse_commandcode_text_tool_calls(text): for pm in re.finditer(r"<[^>]*parameter[^>]*name=\"([^\"]+)\"[^>]*>(.*?)]*parameter>", body, re.DOTALL | re.IGNORECASE): key = (pm.group(1) or "").strip().lower() val = _strip_xmlish_tags(pm.group(2)).strip() - if key == "command": + # [FIX 21] Accept both "command" and "cmd" parameter names. + # The tool schema defines the parameter as "cmd" (see exec_command schema), + # but the model sometimes uses "command" (especially from prefix_rule fallback). + # Previously only "command" was accepted, so DSML blocks with name="cmd" + # were silently dropped — causing Codex CLI to stop mid-task. + if key in ("command", "cmd"): cmd = val elif key == "prefix_rule" and not cmd: try: @@ -1776,6 +1830,15 @@ def _parse_commandcode_text_tool_calls(text): sandbox_permissions = val elif key == "justification": justification = val + + # [FIX 20] Support explore / explore_agent in DSML blocks + is_explore = raw_name.lower() in ("explore", "explore_agent") + if is_explore: + explore_cmd, explore_just = _build_explore_cmd_local(body) + if explore_cmd: + cmd = explore_cmd + justification = explore_just + # Fallback: if the body contains a raw JSON command. if not cmd: jm = re.search(r'"(?:command|cmd)"\s*:\s*"((?:[^"\\]|\\.)*)"', body, re.DOTALL) @@ -1783,7 +1846,9 @@ def _parse_commandcode_text_tool_calls(text): cmd = jm.group(1).replace('\\n', '\n').replace('\\"', '"').strip() if not cmd: continue - tool_name = "exec_command" if raw_name.lower() in ("exec", "bash", "shell", "terminal", "run_command") else raw_name + # [FIX 19] Translate execute_request and other variations to exec_command (CLI only supports exec_command) + # [FIX 20] Translate explore and explore_agent to exec_command + tool_name = "exec_command" if raw_name.lower() in ("exec", "bash", "shell", "terminal", "run_command", "execute_request", "execute_command", "run_shell_command", "run_shell", "run", "explore", "explore_agent") else raw_name args = {"cmd": _unwrap_cmd(cmd)} if sandbox_permissions: args["sandbox_permissions"] = sandbox_permissions if sandbox_permissions in ("use_default", "require_escalated", "with_user_approval") else "require_escalated" @@ -1794,6 +1859,7 @@ def _parse_commandcode_text_tool_calls(text): "name": tool_name, "arguments": json.dumps(args, ensure_ascii=False), }) + # [FIX 16] Native blocks from CommandCode. # Example: # @@ -1848,6 +1914,7 @@ def _parse_commandcode_text_tool_calls(text): "name": "exec_command", "arguments": json.dumps(args, ensure_ascii=False), }) + # [FIX 15] Native blocks from CommandCode. # Format seen in logs: # \nmessages: [{...}]\n @@ -1857,13 +1924,11 @@ def _parse_commandcode_text_tool_calls(text): body = body.strip() msgs = None if body: - # Prefer explicit JSON array after `messages:`; fall back to raw body. try: msgs = json.loads(body) if body.startswith("[") else None except Exception: msgs = None if msgs is None and body: - # Try to extract a JSON array from the body. mm = re.search(r"(\[.*\])", body, re.DOTALL) if mm: try: @@ -1872,28 +1937,37 @@ def _parse_commandcode_text_tool_calls(text): msgs = None if msgs is None: msgs = body - # Convert explore_agent into a real exec_command so downstream clients can execute it. text_for_url = body if isinstance(body, str) else json.dumps(body, ensure_ascii=False) - url_m = re.search(r"https?://[^\s\]'>\"]+", text_for_url) - repo_url = url_m.group(0).rstrip(")].,;'") if url_m else "" - if repo_url: - api_base = repo_url.replace("/admin/", "/api/v1/repos/") - # Build a safe, generic exploration command: README + root contents + releases. - cmd = ( - f"cd /tmp && " - f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | " - f"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); print(base64.b64decode(d['content']).decode())\" 2>/dev/null | head -600 && " - f"curl -sL --max-time 15 '{api_base}/contents' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print('\\n'.join(f'{{x.get(\'path\')}} {{x.get(\'type\')}}' for x in d[:50]))\" 2>/dev/null && " - f"curl -sL --max-time 15 '{api_base}/releases' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(json.dumps(d[:3], indent=2)[:2000])\" 2>/dev/null" - ) - args = {"cmd": cmd, "justification": "Explore repository to understand the app and gather README, root contents, and releases for the landing page."} - else: - args = {"cmd": "echo 'explore_agent: unable to extract repository URL'", "justification": "Fallback for explore_agent block without URL."} + cmd, justification = _build_explore_cmd_local(text_for_url) + if not cmd: + cmd = "echo 'explore_agent: unable to extract repository URL'" + justification = "Fallback for explore_agent block without URL." + args = {"cmd": cmd} + if justification: + args["justification"] = justification calls.append({ "full_match": m.group(0), "name": "exec_command", "arguments": json.dumps(args, ensure_ascii=False), }) + + if not calls and text.count("") >= 2: + url_m = re.search(r"https?://[^\s\]'\\>\"]+", text) + if not url_m: + for prev_url in _last_user_urls: + url_m = re.search(r"https?://[^\s\]'\\>\"]+", prev_url) + if url_m: + break + if url_m: + explore_url = url_m.group(0).rstrip(")].,;'\\") + cmd, justification = _build_explore_cmd_local(explore_url) + if cmd: + calls.append({ + "full_match": "...", + "name": "exec_command", + "arguments": json.dumps({"cmd": cmd, "justification": justification or "Explore repository"}, ensure_ascii=False), + }) + patterns = [ r"\s]+)['\"]?)?>(.*?)", r"(.*?)", @@ -2062,16 +2136,33 @@ def _parse_commandcode_text_tool_calls(text): if not tc_name: continue tc_id = _extract_field(snippet, "id") - tool_name = "exec_command" if tc_name.lower() in ("bash", "shell", "terminal", "run_command") else tc_name - args_raw = _extract_args(snippet) or _extract_field(snippet, "arguments") or _extract_field(snippet, "input") or "{}" - try: - args = json.loads(args_raw) if args_raw.startswith('{') else {"cmd": args_raw} - except Exception: - args = {"cmd": args_raw} - if "cmd" not in args or not args["cmd"]: - args["cmd"] = str(args) - # [FIX 11] Self-healing: unwrap double-wrapped cmd values - args["cmd"] = _unwrap_cmd(args.get("cmd", "")) + + # [FIX 20] Support explore / explore_agent in raw JSON tool calls + is_explore = tc_name.lower() in ("explore", "explore_agent") + + if is_explore: + # Build explore command from the whole snippet/arguments + explore_cmd, explore_just = _build_explore_cmd_local(snippet) + if explore_cmd: + args = {"cmd": explore_cmd} + if explore_just: + args["justification"] = explore_just + else: + args = {"cmd": "echo 'explore: unable to extract repository URL'", "justification": "Fallback for explore tool call without URL."} + tool_name = "exec_command" + else: + # [FIX 19] Translate execute_request and other variations to exec_command (CLI only supports exec_command) + tool_name = "exec_command" if tc_name.lower() in ("exec", "bash", "shell", "terminal", "run_command", "execute_request", "execute_command", "run_shell_command", "run_shell", "run") else tc_name + args_raw = _extract_args(snippet) or _extract_field(snippet, "arguments") or _extract_field(snippet, "input") or "{}" + try: + args = json.loads(args_raw) if args_raw.startswith('{') else {"cmd": args_raw} + except Exception: + args = {"cmd": args_raw} + if "cmd" not in args or not args["cmd"]: + args["cmd"] = str(args) + # [FIX 11] Self-healing: unwrap double-wrapped cmd values + args["cmd"] = _unwrap_cmd(args.get("cmd", "")) + # Normalize sandbox_permissions to valid values _VALID_SP = frozenset({"use_default", "require_escalated", "with_user_approval"}) if "sandbox_permissions" in args: @@ -2100,6 +2191,7 @@ def _parse_commandcode_text_tool_calls(text): "arguments": json.dumps(args, ensure_ascii=False), }) return results + for pat in patterns: for m in re.finditer(pat, text, re.DOTALL | re.IGNORECASE): if pat.startswith("(.*?)", body, re.DOTALL | re.IGNORECASE): key = pm.group(1) or pm.group(2) or "text" params[key] = _strip_xmlish_tags(pm.group(3)).strip() - cmd = params.get("command") or params.get("cmd") or "" + + # [FIX 20] Support explore / explore_agent in XML tool calls + is_explore = raw_name.lower() in ("explore", "explore_agent") + if is_explore: + explore_cmd, explore_just = _build_explore_cmd_local(body) + if explore_cmd: + cmd = explore_cmd + params["justification"] = explore_just + else: + cmd = "" + else: + cmd = params.get("command") or params.get("cmd") or "" + if not cmd and body_stripped.startswith("{"): cm = re.search(r'"(?:command|cmd)"\s*:\s*"(.*?)"\s*,\s*"(?:sandbox_permissions|justification|prefix_rule)"', body, re.DOTALL) if not cm: @@ -2159,7 +2264,9 @@ def _parse_commandcode_text_tool_calls(text): cmd = "\n".join(lines) if not cmd: continue - tool_name = "exec_command" if raw_name.lower() in ("bash", "shell", "terminal", "run_command") else raw_name + # [FIX 19] Translate execute_request and other variations to exec_command (CLI only supports exec_command) + # [FIX 20] Translate explore and explore_agent to exec_command + tool_name = "exec_command" if raw_name.lower() in ("exec", "bash", "shell", "terminal", "run_command", "execute_request", "execute_command", "run_shell_command", "run_shell", "run", "explore", "explore_agent") else raw_name args = {"cmd": _unwrap_cmd(cmd)} # [FIX 11] all paths must unwrap if params.get("sandbox_permissions"): args["sandbox_permissions"] = params["sandbox_permissions"] @@ -2169,6 +2276,42 @@ def _parse_commandcode_text_tool_calls(text): # Also extract raw JSON tool-call objects embedded in free text calls.extend(_extract_raw_json_tool_calls(text)) + + # [FIX 18] Native blocks from the model (used for checklist/task tracking) + # The model outputs a task checklist in a custom XML tag block: + # + # [{"id":"1","status":"in_progress","description":"..."}] + # + # We parse this and map it to a standard 'TodoWrite' tool call so the CLI agent loop continues execution. + for m in re.finditer(r"(.*?)", text, re.DOTALL | re.IGNORECASE): + body = (m.group(1) or "").strip() + if not body: + continue + todos_match = re.search(r"(.*?)", body, re.DOTALL | re.IGNORECASE) + if not todos_match: + continue + raw_todos_json = todos_match.group(1).strip() + try: + raw_todos = json.loads(raw_todos_json) + except Exception as e: + print(f"[translate-proxy] [FIX 18] Failed to parse JSON: {e}", file=sys.stderr) + raw_todos = None + if isinstance(raw_todos, list): + parsed_todos = [] + for item in raw_todos: + if isinstance(item, dict): + desc = item.get("description") or item.get("content") or "" + parsed_todos.append({ + "content": desc, + "activeForm": item.get("activeForm") or desc, + "status": item.get("status") or "pending" + }) + calls.append({ + "full_match": m.group(0), + "name": "TodoWrite", + "arguments": json.dumps({"todos": parsed_todos}, ensure_ascii=False) + }) + # [FIX 11] Self-healing: last-chance sanitization pass on ALL extracted calls calls = _sanitize_tool_calls(calls) return calls @@ -2191,6 +2334,14 @@ def _sanitize_tool_calls(calls): """ cleaned = [] for i, call in enumerate(calls): + # [FIX 18] Skip sanitization pass for non-shell tool calls (e.g., TodoWrite) + # Sanitization specifically validates and repairs command shell executions (the 'cmd' argument). + # Running it on other tools without a 'cmd' parameter (like TodoWrite) would falsely flag + # them as containing JSON garbage or empty commands, corrupting their actual parameters. + if call.get("name") != "exec_command": + cleaned.append(call) + continue + try: args_raw = call.get("arguments", "{}") if isinstance(args_raw, str): @@ -3166,6 +3317,12 @@ class Handler(http.server.BaseHTTPRequestHandler): model = body.get("model", MODELS[0]["id"] if MODELS else "unknown") stream = body.get("stream", False) request_id = body.get("request_id") or body.get("id") or uid("req") + if isinstance(input_data, list): + for item in input_data: + if isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "user": + content = str(item.get("content", "")) + for url_m in re.finditer(r"https?://[^\s\]'\"<>]+", content): + _last_user_urls.append(url_m.group(0)) save_request_snapshot(request_id, body) _req_t0 = time.time() try: @@ -4543,6 +4700,88 @@ if __name__ == "__main__": except Exception as e: _check(f"sanitizer: output valid JSON, got {e}", False) + # Pattern H: Native XML block parsing and sanitization bypass (FIX 18) + _todo_xml = """Some preamble text. + +[{"id":"1","status":"in_progress","description":"Create landing page directory and HTML structure"},{"id":"2","status":"pending","description":"Write the full landing page"}] + +Postamble text.""" + _calls_h = _parse_commandcode_text_tool_calls(_todo_xml) + _check("todo_write: extracted call exists", len(_calls_h) == 1, f"got {len(_calls_h)} calls") + if _calls_h: + _call_h = _calls_h[0] + _check("todo_write: name is TodoWrite", _call_h.get("name") == "TodoWrite") + try: + _args_h = json.loads(_call_h.get("arguments", "{}")) + _todos_h = _args_h.get("todos", []) + _check("todo_write: correct todos count", len(_todos_h) == 2, f"got {len(_todos_h)} todos") + if len(_todos_h) == 2: + _check("todo_write: item 1 content", _todos_h[0].get("content") == "Create landing page directory and HTML structure") + _check("todo_write: item 1 activeForm", _todos_h[0].get("activeForm") == "Create landing page directory and HTML structure") + _check("todo_write: item 1 status", _todos_h[0].get("status") == "in_progress") + _check("todo_write: item 2 status", _todos_h[1].get("status") == "pending") + # Confirm that the arguments contain no 'cmd' or sanitization comment + _check("todo_write: no cmd injected", "cmd" not in _args_h) + except Exception as e: + _check(f"todo_write: parsed JSON error: {e}", False) + + # Pattern I: Translate execute_request to exec_command (FIX 19) + _exec_req_raw = '<||DSML||tool_calls>\n<||DSML||invoke name="execute_request">\n<||DSML||parameter name="command" string="true">ls -la\n\n' + _calls_i = _parse_commandcode_text_tool_calls(_exec_req_raw) + _check("execute_request: mapped successfully", len(_calls_i) == 1, f"got {len(_calls_i)} calls") + if _calls_i: + _call_i = _calls_i[0] + _check("execute_request: name translated to exec_command", _call_i.get("name") == "exec_command", f"got {_call_i.get('name')}") + try: + _args_i = json.loads(_call_i.get("arguments", "{}")) + _check("execute_request: correct command extracted", _args_i.get("cmd") == "ls -la", f"got {_args_i.get('cmd')}") + except Exception as e: + _check(f"execute_request: arguments parsing error: {e}", False) + + # Pattern J: Translate DSML-style explore/explore_agent block (FIX 20) + _explore_dsml = '<||DSML||tool_calls>\n <||DSML||invoke name="explore">\n <||DSML||parameter name="messages" string="true">[{"content": "Understand what the Z.AI-Chat-for-Android project is about... URL: https://github.rommark.dev/admin/Z.AI-Chat-for-Android", "role": "user"}]\n \n ' + _calls_j = _parse_commandcode_text_tool_calls(_explore_dsml) + _check("explore DSML: mapped successfully", len(_calls_j) == 1, f"got {len(_calls_j)} calls") + if _calls_j: + _call_j = _calls_j[0] + _check("explore DSML: name translated to exec_command", _call_j.get("name") == "exec_command", f"got {_call_j.get('name')}") + try: + _args_j = json.loads(_call_j.get("arguments", "{}")) + _check("explore DSML: built a curl explore script targeting api base", "api/v1/repos/admin/Z.AI-Chat-for-Android" in _args_j.get("cmd", ""), f"got {_args_j.get('cmd')!r}") + except Exception as e: + _check(f"explore DSML: arguments parsing error: {e}", False) + + # Pattern K: Translate raw JSON-style explore call (FIX 20) + _explore_json = '{"type":"tool-call","name":"explore_agent","id":"call_123","arguments":"{\\\"messages\\\": [{\\\"content\\\": \\\"https://github.rommark.dev/admin/Z.AI-Chat-for-Android\\\"}]}"}' + _calls_k = _parse_commandcode_text_tool_calls(_explore_json) + _check("explore JSON: mapped successfully", len(_calls_k) == 1, f"got {len(_calls_k)} calls") + if _calls_k: + _call_k = _calls_k[0] + _check("explore JSON: name translated to exec_command", _call_k.get("name") == "exec_command") + try: + _args_k = json.loads(_call_k.get("arguments", "{}")) + _check("explore JSON: built a curl explore script targeting api base", "api/v1/repos/admin/Z.AI-Chat-for-Android" in _args_k.get("cmd", ""), f"got {_args_k.get('cmd')!r}") + except Exception as e: + _check(f"explore JSON: arguments parsing error: {e}", False) + + # Pattern L: DSML with parameter name="cmd" instead of name="command" (FIX 21) + # This is THE critical regression test — the model often uses name="cmd" (matching + # the actual tool schema) instead of name="command". Previously the DSML parser + # silently dropped these, causing Codex CLI to halt mid-task. + _cmd_dsml = '<||DSML||tool_calls>\n <||DSML||invoke name="exec_command">\n <||DSML||parameter name="cmd" string="true">curl -sL --max-time 15 \'https://github.rommark.dev/api/v1/repos/admin/Z.AI-Chat-for-Android/contents/README.md\' 2>/dev/null\n <||DSML||parameter name="sandbox_permissions" string="true">require_escalated\n <||DSML||parameter name="justification" string="true">I need to get the README from the private repo to understand the Android app before building the landing page mockup.\n \n ' + _calls_l = _parse_commandcode_text_tool_calls(_cmd_dsml) + _check("DSML name=cmd: mapped successfully", len(_calls_l) == 1, f"got {len(_calls_l)} calls") + if _calls_l: + _call_l = _calls_l[0] + _check("DSML name=cmd: name is exec_command", _call_l.get("name") == "exec_command", f"got {_call_l.get('name')}") + try: + _args_l = json.loads(_call_l.get("arguments", "{}")) + _check("DSML name=cmd: cmd extracted correctly", "curl -sL --max-time 15" in _args_l.get("cmd", ""), f"got {_args_l.get('cmd')!r}") + _check("DSML name=cmd: sandbox_permissions extracted", _args_l.get("sandbox_permissions") == "require_escalated", f"got {_args_l.get('sandbox_permissions')!r}") + _check("DSML name=cmd: justification extracted", "README" in _args_l.get("justification", ""), f"got {_args_l.get('justification')!r}") + except Exception as e: + _check(f"DSML name=cmd: arguments parsing error: {e}", False) + print(f"[CC-SELF-TEST] Results: {_counts[0]} passed, {_counts[1]} failed", file=sys.stderr) if _counts[1]: