From 19994f9f1f2632c4733ec505d8315aadadd9c126 Mon Sep 17 00:00:00 2001 From: costin-uipath <48773677+costin-uipath@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:21:21 +0300 Subject: [PATCH 1/4] fix(codex): recover v2 sub-agent tool calls - Register the child from a subAgentActivity `started` item; drop the other kinds. - Read shell commands out of v2 `exec` scripts in the child rollout. - Mark failed `exec` (Script failed) and shell (Exit code: N) calls as errors. - Map the v1 `shell_command` function call to Bash. - Skip the parent history a forked child rollout starts with. Co-Authored-By: Claude Opus 5.5 --- .claude/notes/agents.md | 24 +++ src/coder_eval/agents/codex_agent.py | 194 ++++++++++++++++- tests/test_codex_agent.py | 300 +++++++++++++++++++++++++++ 3 files changed, 516 insertions(+), 2 deletions(-) diff --git a/.claude/notes/agents.md b/.claude/notes/agents.md index 365fc01d..456076c9 100644 --- a/.claude/notes/agents.md +++ b/.claude/notes/agents.md @@ -395,6 +395,30 @@ The rollout file can lag the parent's `turn/completed` by a beat (the recorder f a background task), so the lookup polls ~2 s — but bails immediately when `/sessions` does not exist at all, since no flush can ever land there. +Codex multi-agent v2 moved the spawn off the collab item. A v1 spawn is a +`collabAgentToolCall` that names the child in `receiver_thread_ids`; a v2 spawn is a +`subAgentActivity` item with `kind: started`, `id` = the spawn call id and +`agent_thread_id` = the child, and its `wait` names no child at all. Registration keys off +both items: keyed off the collab item alone, a v2 run recovers nothing the child ran. The +other `subAgentActivity` kinds (`completed`, `interrupted`, `interacted`) are notices about +a registered child, dropped before telemetry so they neither become unknown rows nor open +a parent model call. + +The child's shell calls come in two shapes. v1 records a `shell_command` function call +with JSON `arguments` and an output starting `Exit code: N`. v2 records a custom tool +`exec` whose `input` is a JavaScript script calling the shell tool, often several in a +`Promise.all`, and whose output is a list of content parts starting `Script completed` / +`Script failed`. The script is read by hand: bare keys and single quotes are the norm, and +`json.loads` recovers about a third. A command built from a variable falls back to the +whole script as the command text, which still matches as text. Both outputs are read so a +failed call is not recorded as a success. + +A spawn with `fork_turns` copies the parent's history, tool calls included, into the head +of the child's rollout. In a forked rollout (`session_meta` carries `forked_from_id`), +parsing starts at the first `thread_settings_applied` with the child's thread id, so the +parent's commands are not credited to the child. A non-forked rollout is parsed whole: +that event also appears mid-thread on a settings change. + The thread-cumulative baseline is the other half. `ThreadTokenUsage.total` counts the whole THREAD, and the Codex thread is created once per task and reused, so by turn N it still carries turns 1..N-1. The orchestrator sums per-turn usages, so handing it the cumulative diff --git a/src/coder_eval/agents/codex_agent.py b/src/coder_eval/agents/codex_agent.py index e37bc73a..7ef5a796 100644 --- a/src/coder_eval/agents/codex_agent.py +++ b/src/coder_eval/agents/codex_agent.py @@ -5,6 +5,7 @@ import json import logging import os +import re import shlex import shutil import tempfile @@ -116,6 +117,7 @@ "commandExecution": "Bash", "fileChange": "Write", "collabAgentToolCall": "Agent", + "subAgentActivity": "Agent", "mcpToolCall": "Mcp", "dynamicToolCall": "Tool", "webSearch": "WebSearch", @@ -128,6 +130,11 @@ # to recover. Lowercased to match _status_value's normalization. _COLLAB_SPAWN_TOOL = "spawnagent" +# A subAgentActivity `started` item is the spawn; its other kinds are notices. +# Rationale: .claude/notes/agents.md § Codex rollout rebuild +_SUB_AGENT_ACTIVITY_ITEM = "subAgentActivity" +_SUB_AGENT_SPAWN_KIND = "started" + # Tool names for the raw ResponseItem function calls in a sub-agent's on-disk # rollout, which is the only place its inner tool calls survive. # Rationale: .claude/notes/agents.md § Codex rollout rebuild @@ -135,11 +142,22 @@ "exec_command": "Bash", "shell": "Bash", "local_shell": "Bash", + "shell_command": "Bash", + # A script: _exec_shell_commands reads the commands out of it. + "exec": "Bash", "apply_patch": "Write", "spawn_agent": "Agent", "wait_agent": "Agent", } +# The shell call inside an exec script; the object literal follows. cli 0.140-0.147 +# wrote `tools.shell_command({command: …})`, cli 0.162 writes +# `tools.exec_command({"cmd": …})` — both names and both keys are accepted. +_EXEC_SHELL_CALL = re.compile(r"tools\.(?:shell_command|exec_command)\s*\(") +# The command key of that literal, quoted or bare (the script is JavaScript). +_EXEC_COMMAND_KEY = re.compile(r"""(?:["'](?:command|cmd)["']|\b(?:command|cmd))\s*:\s*""") +_JS_ESCAPES = {"n": "\n", "t": "\t", "r": "\r", "0": "\0", "b": "\b", "f": "\f", "v": "\v"} + # Rollout ResponseItem payload types that are sub-agent tool CALLS and their # matching OUTPUT type (keyed by `call_id`). Anything not listed is skipped. _ROLLOUT_TOOL_CALL_TYPES = frozenset({"function_call", "local_shell_call", "custom_tool_call"}) @@ -213,6 +231,90 @@ def _status_value(status: Any) -> str: return str(value).lower() if value is not None else "" +def _js_string_at(text: str, i: int) -> tuple[str, int] | None: + """Read the JavaScript string literal starting at ``text[i]``. + + Returns (decoded value, index after the closing quote), or None when ``text[i]`` + is not a quote or the literal never closes. Single, double and backtick quotes; + backslash escapes decoded (``\\n``, ``\\"``, ``\\\\``, ``\\uXXXX``); a ``${}`` + inside a template literal stays verbatim. + """ + if i >= len(text) or text[i] not in "\"'`": + return None + quote = text[i] + out: list[str] = [] + j = i + 1 + while j < len(text): + ch = text[j] + if ch == "\\" and j + 1 < len(text): + esc = text[j + 1] + if esc == "u" and j + 5 < len(text): + with contextlib.suppress(ValueError): + out.append(chr(int(text[j + 2 : j + 6], 16))) + j += 6 + continue + out.append(_JS_ESCAPES.get(esc, esc)) + j += 2 + continue + if ch == quote: + return "".join(out), j + 1 + out.append(ch) + j += 1 + return None + + +def _matching_brace(text: str, start: int) -> int: + """Index just past the ``}`` matching ``text[start] == "{"``, skipping strings. + + Falls back to ``len(text)`` for an unbalanced literal, so a truncated script + still yields whatever it holds. + """ + depth = 0 + i = start + while i < len(text): + ch = text[i] + if ch in "\"'`": + read = _js_string_at(text, i) + if read is None: + return len(text) + i = read[1] + continue + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return len(text) + + +def _exec_shell_commands(script: str) -> list[str]: + """Every literal command an exec script hands to the shell tool. + + A command built from a variable is skipped. + Rationale: .claude/notes/agents.md § Codex rollout rebuild + """ + commands: list[str] = [] + for call in _EXEC_SHELL_CALL.finditer(script): + brace = script.find("{", call.end()) + if brace < 0: + continue + body = script[brace : _matching_brace(script, brace)] + key = _EXEC_COMMAND_KEY.search(body) + if key is None: + continue + read = _js_string_at(body, key.end()) + if read is not None and read[0]: + commands.append(read[0]) + return commands + + +def _is_subagent_lifecycle_notice(root: Any, root_type: str | None) -> bool: + """True for a subAgentActivity item other than its `started` spawn.""" + return root_type == _SUB_AGENT_ACTIVITY_ITEM and _status_value(getattr(root, "kind", "")) != _SUB_AGENT_SPAWN_KIND + + def _fresh_input_tokens(raw_input: int, cached: int) -> int: """The fresh (uncached) prompt slice = tokens written to cache this call. @@ -601,6 +703,8 @@ def on_item_started(self, notification: Any) -> None: if item_id is not None and started_at_ms is not None: self.start_ms_by_id[item_id] = started_at_ms root_type = getattr(root, "type", None) + if _is_subagent_lifecycle_notice(root, root_type): + return if not self.in_api_call and root_type not in _NON_MODEL_ITEM_TYPES: self._open_api_call() # Any item that isn't transcript content is a tool call (generic capture). @@ -636,6 +740,8 @@ def on_item_completed(self, notification: Any) -> None: return completed_ms = getattr(notification.payload, "completed_at_ms", None) root_type = getattr(root, "type", None) + if _is_subagent_lifecycle_notice(root, root_type): + return if root_type is not None and root_type not in _CONTENT_ITEM_TYPES: tool_id = getattr(root, "id", None) or f"{root_type}_{self.next_sequence}" seq = self.seq_by_id.get(tool_id, self.next_sequence) @@ -688,6 +794,13 @@ def on_item_completed(self, notification: Any) -> None: self.spawned_children, self.collab_results, ) + elif root_type == _SUB_AGENT_ACTIVITY_ITEM: + self._agent._handle_subagent_spawn( + root, + tool_id, + self.collab_spawn_by_thread, + self.spawned_children, + ) elif root_type == "reasoning": # OpenAI never returns raw CoT, so a text-less item becomes a @@ -1751,6 +1864,15 @@ def _tool_parameters(self, root: Any, root_type: str | None) -> dict[str, Any]: if prompt := getattr(root, "prompt", None): params["prompt"] = prompt return params + if root_type == _SUB_AGENT_ACTIVITY_ITEM: + # Only the `started` kind reaches here; it is the spawn call. + params = { + "operation": _COLLAB_SPAWN_TOOL, + "agent_thread_id": str(getattr(root, "agent_thread_id", "") or ""), + } + if agent_path := getattr(root, "agent_path", None): + params["agent_path"] = str(agent_path) + return params if root_type in ("mcpToolCall", "dynamicToolCall"): params = {"tool": getattr(root, "tool", "")} if server := (getattr(root, "server", None) or getattr(root, "namespace", None)): @@ -1813,6 +1935,10 @@ def _extract_generic_telemetry( """ try: status_str = _status_value(getattr(root, "status", "") or "") + if root_type == _SUB_AGENT_ACTIVITY_ITEM and not status_str: + # Codex emits the spawn activity only once the child thread + # exists, so a status-less item is a successful spawn. + status_str = "completed" err = getattr(root, "error", None) success = getattr(root, "success", None) is_error = bool(err) or success is False or status_str in _FILE_CHANGE_FAILURE_STATUSES @@ -1845,6 +1971,9 @@ def _summarize_tool_item(root: Any, root_type: str | None) -> str: states = getattr(root, "agents_states", None) or {} messages = [s.message for s in states.values() if getattr(s, "message", None)] return f"collab {op}: {'; '.join(messages)[:200]}" if messages else f"collab {op}" + if root_type == _SUB_AGENT_ACTIVITY_ITEM: + target = getattr(root, "agent_path", "") or getattr(root, "agent_thread_id", "") + return f"collab {_COLLAB_SPAWN_TOOL}: {target}" if root_type == "webSearch": return f"query: {getattr(root, 'query', '')}" if root_type in ("mcpToolCall", "dynamicToolCall"): @@ -1887,6 +2016,26 @@ def _handle_collab_completion( if message and thread_id in spawn_by_thread: collab_results[str(thread_id)] = str(message) + def _handle_subagent_spawn( + self, + root: Any, + tool_id: str, + spawn_by_thread: dict[str, str], + spawned_children: list[tuple[str, str, str | None]], + ) -> None: + """Register the child a `started` subAgentActivity item announces, under ``tool_id``. + + Idempotent per thread. Rationale: .claude/notes/agents.md § Codex rollout rebuild + """ + thread_id = getattr(root, "agent_thread_id", None) + if not thread_id: + return + thread_id = str(thread_id) + if thread_id in spawn_by_thread: + return + spawn_by_thread[thread_id] = tool_id + spawned_children.append((thread_id, tool_id, None)) + async def _recover_subagent_tool_calls( self, spawned_children: list[tuple[str, str, str | None]], @@ -1997,6 +2146,7 @@ def _parse_rollout_generations(cls, path: Path) -> list[dict[str, Any]]: objs.append(json.loads(raw)) except json.JSONDecodeError: continue + objs = objs[cls._fork_boundary(objs) :] # Pass 1: tool OUTPUTs by call_id. outputs: dict[str, tuple[str, bool]] = {} @@ -2056,6 +2206,31 @@ def close(tokens: tuple[int, int, int, int] | None) -> None: close(None) return gens + @staticmethod + def _fork_boundary(objs: list[dict[str, Any]]) -> int: + """Index where the child's own history starts. + + A spawn with ``fork_turns`` copies the parent's history into the child's + rollout first, parent tool calls included; replaying those would credit + them to the child. The copy ends at the ``thread_settings_applied`` event + carrying the rollout's own thread id (its first ``session_meta``); 0 when + the rollout is not a fork or has no such event. + Rationale: .claude/notes/agents.md § Codex rollout rebuild + """ + meta = next(((o.get("payload") or {}) for o in objs if o.get("type") == "session_meta"), {}) + own = meta.get("id") + if not own or not meta.get("forked_from_id"): + return 0 + for i, o in enumerate(objs): + p = o.get("payload") or {} + if ( + o.get("type") == "event_msg" + and p.get("type") == "thread_settings_applied" + and p.get("thread_id") == own + ): + return i + return 0 + @staticmethod def _message_text(content: Any) -> str: """Join the text of a rollout ``message`` ResponseItem's content parts.""" @@ -2081,8 +2256,13 @@ def _subagent_parameters(payload: dict[str, Any]) -> dict[str, Any]: """Best-effort parameters for a rollout tool-call ResponseItem. Shell-style calls are normalized to ``{"command": ...}`` so the transcript - renders the command line; everything else passes through. + renders the command line; everything else passes through. An ``exec`` + script yields its literal commands, one per line, or itself when it has none. """ + if payload.get("name") == "exec" and isinstance(payload.get("input"), str): + script = payload["input"] + commands = _exec_shell_commands(script) + return {"command": "\n".join(commands) if commands else script} args = payload.get("arguments") if isinstance(args, str) and args: with contextlib.suppress(json.JSONDecodeError): @@ -2106,9 +2286,19 @@ def _subagent_parameters(payload: dict[str, Any]) -> dict[str, Any]: @staticmethod def _subagent_output(payload: dict[str, Any]) -> tuple[str, bool]: - """(result_summary, is_error) for a rollout tool-output ResponseItem.""" + """(result_summary, is_error) for a rollout tool-output ResponseItem. + + An exec output is a list of content parts starting ``Script completed`` or + ``Script failed``; a shell output is a string starting ``Exit code: N``. + """ output = payload.get("output") is_error = False + if isinstance(output, list): + text = "".join(str(part.get("text", "")) if isinstance(part, dict) else str(part) for part in output) + is_error = text.lstrip().startswith("Script failed") + return text, is_error + if isinstance(output, str) and (exit_code := re.match(r"Exit code: (\d+)", output)): + return output, exit_code.group(1) != "0" if isinstance(output, dict): if output.get("success") is False: is_error = True diff --git a/tests/test_codex_agent.py b/tests/test_codex_agent.py index e2cf8a06..8aaaac49 100644 --- a/tests/test_codex_agent.py +++ b/tests/test_codex_agent.py @@ -1069,6 +1069,145 @@ def _collab_call( ) +class TestExecScriptCommandExtraction: + """Shell commands are read out of an `exec` script so recovered sub-agent rows are Bash rows.""" + + def test_json_quoted_keys(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = ( + 'const r = await tools.shell_command({"command":"git status --short --branch",' + '"workdir":"C:\\\\w","timeout_ms":10000}); text(r);' + ) + assert _exec_shell_commands(script) == ["git status --short --branch"] + + def test_bare_keys_single_quotes_and_escapes(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = ( + 'const a = await tools.shell_command({command:\'pytest --rootdir "C:\\\\p" ' + '-q tests\',workdir:"C:\\\\repo"}); text(a);' + ) + assert _exec_shell_commands(script) == ['pytest --rootdir "C:\\p" -q tests'] + + def test_promise_all_yields_every_command_in_order(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = ( + "const r = await Promise.all([\n" + ' tools.shell_command({command:"git branch --show-current",workdir:"C:\\\\w"}),\n' + ' tools.shell_command({command:"git status --short",workdir:"C:\\\\w"}),\n' + "]);" + ) + assert _exec_shell_commands(script) == ["git branch --show-current", "git status --short"] + + def test_newline_escape_and_template_literal(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = 'await tools.shell_command({command:`echo "a"\\nls -la`})' + assert _exec_shell_commands(script) == ['echo "a"\nls -la'] + + def test_variable_command_is_not_a_literal(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = 'const cmd = "npm run build"; const r = await tools.shell_command({command: cmd});' + assert _exec_shell_commands(script) == [] + + def test_other_tools_are_ignored(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = ( + 'await tools.update_plan({plan:[{step:"x",status:"in_progress"}]}); await tools.view_image({path:"a.png"});' + ) + assert _exec_shell_commands(script) == [] + + def test_exec_command_with_cmd_key(self): + from coder_eval.agents.codex_agent import _exec_shell_commands + + script = ( + 'const r = await tools.exec_command({"cmd":"git log --oneline -1","workdir":"C:\\\\w",' + '"yield_time_ms":10000,"max_output_tokens":2000});\ntext(r.output);\n' + ) + assert _exec_shell_commands(script) == ["git log --oneline -1"] + + def test_exec_output_parts_are_joined_and_a_failed_script_is_an_error(self): + ok = { + "type": "custom_tool_call_output", + "call_id": "c1", + "output": [ + {"type": "input_text", "text": "Script completed\nWall time 1.0 seconds\nOutput:\n"}, + {"type": "input_text", "text": "ok\n"}, + ], + } + failed = { + "type": "custom_tool_call_output", + "call_id": "c2", + "output": [ + {"type": "input_text", "text": "Script failed\nWall time 1.6 seconds\nOutput:\n"}, + {"type": "input_text", "text": "boom\n"}, + ], + } + text, is_error = CodexAgent._subagent_output(ok) + assert "ok" in text + assert not is_error + text, is_error = CodexAgent._subagent_output(failed) + assert "boom" in text + assert is_error + + def test_v1_exit_code_output_marks_non_zero_as_error(self): + assert CodexAgent._subagent_output({"output": "Exit code: 0\nWall time: 1 seconds\nOutput:\nok\n"}) == ( + "Exit code: 0\nWall time: 1 seconds\nOutput:\nok\n", + False, + ) + assert CodexAgent._subagent_output({"output": "Exit code: 1\nOutput:\nboom\n"})[1] is True + assert CodexAgent._subagent_output({"output": "5050\n"}) == ("5050\n", False) + + def test_rollout_exec_call_becomes_a_bash_row_with_the_command(self): + payload = { + "type": "custom_tool_call", + "name": "exec", + "call_id": "call_x", + "input": ('const r = await tools.shell_command({command:"pytest tests/test_api.py -q"}); text(r);'), + } + assert CodexAgent._subagent_tool_name(payload) == "Bash" + assert CodexAgent._subagent_parameters(payload) == {"command": "pytest tests/test_api.py -q"} + + def test_rollout_exec_call_without_a_literal_keeps_the_script_as_command_text(self): + script = 'const cmd = "npm run build"; await tools.shell_command({command: cmd});' + payload = {"type": "custom_tool_call", "name": "exec", "call_id": "call_y", "input": script} + assert CodexAgent._subagent_parameters(payload) == {"command": script} + + def test_v1_shell_command_function_call_is_bash(self): + payload = { + "type": "function_call", + "name": "shell_command", + "call_id": "call_z", + "arguments": '{"command":"git status --short","workdir":"C:\\\\w","timeout_ms":10000}', + } + assert CodexAgent._subagent_tool_name(payload) == "Bash" + assert CodexAgent._subagent_parameters(payload)["command"] == "git status --short" + + +def _sub_agent_activity( + kind: str, + *, + item_id: str, + child_thread: str, + agent_path: str = "agent-1", +) -> SimpleNamespace: + """A subAgentActivity item root (Codex multi-agent v2's sub-agent lifecycle item). + + ``kind`` is "started" (the spawn itself: ``item_id`` is the spawn_agent call id) + or a lifecycle notice ("completed", "interrupted", "interacted").""" + return SimpleNamespace( + type="subAgentActivity", + id=item_id, + kind=kind, + agent_thread_id=child_thread, + agent_path=agent_path, + ) + + class TestCodexCollabSubAgent: """Codex spawns sub-agents via collabAgentToolCall (tool='spawnAgent'), not Claude's Task tool. The agent must surface these as tool calls AND nest the @@ -1240,6 +1379,167 @@ async def test_inner_shell_command_recovered_and_nested(self, monkeypatch, tmp_p # Both the returned text ("5050") and the recovered tool-call message nest. assert any(sub_tool_id in m.tool_use_ids for m in nested) + async def test_v2_started_activity_is_the_spawn_and_feeds_recovery(self, monkeypatch, tmp_path): + # The wait names no child: only the `started` item can register it. + monkeypatch.setenv("CODEX_HOME", str(tmp_path)) + child = "019e0000-dddd-7000-8000-000000000004" + _write_child_rollout( + tmp_path, + child, + [ + { + "type": "custom_tool_call", + "name": "exec", + "call_id": "c_1", + "input": 'await tools.exec_command({"cmd":"pytest tests/test_api.py -q"})', + }, + { + "type": "custom_tool_call_output", + "call_id": "c_1", + "output": [ + {"type": "input_text", "text": "Script completed\nWall time 3.0 seconds\nOutput:\n"}, + {"type": "input_text", "text": "1 passed in 0.31s\n"}, + ], + }, + ], + ) + started = _sub_agent_activity("started", item_id="call_spawn", child_thread=child) + wait = SimpleNamespace( + type="collabAgentToolCall", + id="call_wait", + tool="wait", + model=None, + prompt=None, + status="completed", + receiver_thread_ids=[], + agents_states={}, + ) + completed = _sub_agent_activity("completed", item_id="subagent-completed-turn_c1", child_thread=child) + notifications = [ + _item_notification("item/started", started), + _item_notification("item/completed", started), + _item_notification("item/started", wait), + _item_notification("item/completed", wait), + _item_notification("item/started", completed), + _item_notification("item/completed", completed), + _delta("done"), + _turn_completed(), + ] + agent = _started_agent(parse_agent_config(type=AgentKind.CODEX, model="parent-model"), notifications) + + record = await agent.communicate("delegate it") + + # The spawn and the wait are the only Agent rows: the `completed` notice + # is neither an Agent row nor a raw "subAgentActivity" stub. + agent_rows = {c.tool_id: c for c in record.commands if c.tool_name == "Agent"} + assert set(agent_rows) == {"call_spawn", "call_wait"} + assert not [c for c in record.commands if c.tool_name == "subAgentActivity"] + assert not [c for c in record.commands if c.tool_id.startswith("subagent-completed")] + spawn_row = agent_rows["call_spawn"] + assert spawn_row.parameters["operation"] == "spawnagent" + assert spawn_row.parameters["agent_thread_id"] == child + assert spawn_row.result_status == "success" + + # The child's command is recovered from its rollout and nests under the spawn. + bash = [c for c in record.commands if c.tool_name == "Bash"] + assert len(bash) == 1 + assert bash[0].parameters["command"] == "pytest tests/test_api.py -q" + assert bash[0].result_status == "success" + assert "1 passed" in (bash[0].result_summary or "") + nested = [m for m in record.messages if getattr(m, "parent_tool_use_id", None) == "call_spawn"] + assert any(bash[0].tool_id in m.tool_use_ids for m in nested) + assert all(m.model == "parent-model" for m in nested) + + async def test_v2_lifecycle_notices_without_a_spawn_record_nothing(self, monkeypatch, tmp_path): + # `completed` / `interrupted` without a `started` are notices about a + # child this turn did not spawn: no tool rows, no sub-agent, no crash. + monkeypatch.setenv("CODEX_HOME", str(tmp_path)) + child = "019e0000-eeee-7000-8000-000000000005" + completed = _sub_agent_activity("completed", item_id="subagent-completed-turn_x", child_thread=child) + interrupted = _sub_agent_activity("interrupted", item_id="subagent-interrupted-turn_x", child_thread=child) + notifications = [ + _item_notification("item/started", completed), + _item_notification("item/completed", completed), + _item_notification("item/started", interrupted), + _item_notification("item/completed", interrupted), + _delta("done"), + _turn_completed(), + ] + agent = _started_agent(parse_agent_config(type=AgentKind.CODEX), notifications) + + record = await agent.communicate("go") + + assert record.commands == [] + assert not any(getattr(m, "parent_tool_use_id", None) for m in record.messages) + + async def test_v2_repeated_started_notice_registers_the_child_once(self, monkeypatch, tmp_path): + monkeypatch.setenv("CODEX_HOME", str(tmp_path)) + child = "019e0000-ffff-7000-8000-000000000006" + _write_child_rollout( + tmp_path, + child, + [ + {"type": "function_call", "name": "exec_command", "call_id": "c_1", "arguments": '{"cmd":"echo one"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "one\n"}, + ], + ) + started = _sub_agent_activity("started", item_id="call_spawn", child_thread=child) + notifications = [ + _item_notification("item/started", started), + _item_notification("item/completed", started), + _item_notification("item/started", started), + _item_notification("item/completed", started), + _delta("done"), + _turn_completed(), + ] + agent = _started_agent(parse_agent_config(type=AgentKind.CODEX), notifications) + + record = await agent.communicate("go") + + # One registration → the rollout is mined once; the command is not doubled. + assert len([c for c in record.commands if c.tool_name == "Bash"]) == 1 + + def test_forked_parent_history_is_not_credited_to_the_child(self, tmp_path): + # fork_turns copies the parent's history ahead of the child's own; the + # parent's shell call in that prefix must not be replayed as the child's. + child, parent = "019e0000-1111-7000-8000-000000000008", "019e0000-0000-7000-8000-000000000000" + _write_child_rollout( + tmp_path, + child, + [ + {"type": "session_meta", "payload": {"id": child, "forked_from_id": parent}}, + {"type": "session_meta", "payload": {"id": parent}}, + { + "type": "function_call", + "name": "shell_command", + "call_id": "p_1", + "arguments": '{"command":"git status"}', + }, + {"type": "function_call_output", "call_id": "p_1", "output": "Exit code: 0\nOutput:\n"}, + {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, + {"type": "turn_context", "payload": {"turn_id": "t_child"}}, + {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\nREADME.md\n"}, + ], + ) + gens = CodexAgent._parse_rollout_generations(CodexAgent._find_rollout_file(tmp_path, child)) + assert [t["parameters"]["command"] for g in gens for t in g["tools"]] == ["ls"] + + def test_rollout_without_fork_marker_is_parsed_whole(self, tmp_path): + child = "019e0000-2222-7000-8000-000000000009" + _write_child_rollout( + tmp_path, + child, + [ + {"type": "session_meta", "payload": {"id": child}}, + {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\n"}, + {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, + ], + ) + gens = CodexAgent._parse_rollout_generations(CodexAgent._find_rollout_file(tmp_path, child)) + assert [t["parameters"]["command"] for g in gens for t in g["tools"]] == ["ls"] + async def test_missing_rollout_is_silently_skipped(self, monkeypatch, tmp_path): # No rollout written for the child → recovery finds nothing and the turn # still completes with just the returned result nested (no crash). From 6567464574a86518950270ccd5c723c84439ff13 Mon Sep 17 00:00:00 2001 From: costin-uipath <48773677+costin-uipath@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:33:55 +0300 Subject: [PATCH 2/4] fix(codex): read a sub-agent's commands from its rollout records A Codex sub-agent runs on its own thread. The parent stream never carries its tool calls, and since multi-agent v2 the child's shell no longer appears as function calls in its rollout either. Its commands now come from the records Codex writes while the child runs. - A multi-agent v2 `subAgentActivity` `started` item is mapped onto the v1 spawn shape where items enter the turn state, so one path registers a child from either item kind, and registers it once. The other kinds (`completed`, `interrupted`, `interacted`) are notices and record nothing. - A code-mode `exec` script's rows are the `item_completed` records Codex writes while it runs: one `Bash` row per `CommandExecution`, with the record's exit code and the shell wrapper stripped (`/bin/zsh -lc`, `powershell.exe -Command`, `pwsh.exe -Command`), and one `Write` row per `FileChange`. A script that left no record stays one `exec` row. - A record is matched to the newest script whose source holds its command, compared with backslashes removed, because the JavaScript source escapes the backslashes and double quotes the command holds. A record that matches no source goes to the newest script. Each script's records take the place of its row. - A direct `shell_command` or `exec_command` call takes its status from its output header (`Exit code: N`, `Process exited with code N`). A call whose output says the call itself failed (`exec_command failed: ...`, `write_stdin failed: ...`) is an error. A call with no result is `unknown`, whether an interrupt cut it off or it still ran after its yield time; the `wait` or `write_stdin` call that later returns its result is a row of its own with that result. - A forked child rollout without its own `thread_settings_applied` is parsed whole, with a warning. - Docs: a sub-agent section in CODEX.md, a sub-agent table in HARNESS_PARITY.md, and the rollout-rebuild note describes the records, the match, the yield semantics and which CLI versions write records (none through 0.147; 0.156.1 and 0.162 do). Verified on real child rollouts (CLI 0.156.1 and 0.162, Windows) and with an end-to-end run of skill-migrator-migrate-with-fixes-e2e on CLI 0.156.1: the spawned child's seven commands and one patch were recovered as rows under the spawn, each with its own result. Co-Authored-By: Claude Fable 5.1 --- .claude/notes/agents.md | 71 ++++- docs/agents/CODEX.md | 24 ++ docs/agents/HARNESS_PARITY.md | 12 + src/coder_eval/agents/codex_agent.py | 450 +++++++++++++++------------ tests/_fixtures/codex_rollouts.py | 113 +++++++ tests/test_codex_agent.py | 404 ++++++++---------------- tests/test_codex_agent_unit.py | 331 +++++++++++++++++++- 7 files changed, 926 insertions(+), 479 deletions(-) create mode 100644 tests/_fixtures/codex_rollouts.py diff --git a/.claude/notes/agents.md b/.claude/notes/agents.md index 456076c9..6bb0bfef 100644 --- a/.claude/notes/agents.md +++ b/.claude/notes/agents.md @@ -373,7 +373,8 @@ Codex runs every sub-agent on its own child thread whose events never reach the stream, and that thread persists with *Limited* rollout policy, which drops `commandExecution` events. So neither the live stream nor `thread.read` surfaces the sub-agent's shell commands, and `thread/tokenUsage/updated` only ever reports the PARENT -thread. +thread. (The commands a code-mode script runs are the exception: they persist as +`item_completed` records, below.) But the child rollout ALWAYS persists the raw `function_call` / `local_shell_call` / `custom_tool_call` ResponseItems and a `token_count` event with the child's cumulative @@ -398,26 +399,64 @@ a background task), so the lookup polls ~2 s — but bails immediately when Codex multi-agent v2 moved the spawn off the collab item. A v1 spawn is a `collabAgentToolCall` that names the child in `receiver_thread_ids`; a v2 spawn is a `subAgentActivity` item with `kind: started`, `id` = the spawn call id and -`agent_thread_id` = the child, and its `wait` names no child at all. Registration keys off -both items: keyed off the collab item alone, a v2 run recovers nothing the child ran. The -other `subAgentActivity` kinds (`completed`, `interrupted`, `interacted`) are notices about -a registered child, dropped before telemetry so they neither become unknown rows nor open -a parent model call. - -The child's shell calls come in two shapes. v1 records a `shell_command` function call -with JSON `arguments` and an output starting `Exit code: N`. v2 records a custom tool -`exec` whose `input` is a JavaScript script calling the shell tool, often several in a -`Promise.all`, and whose output is a list of content parts starting `Script completed` / -`Script failed`. The script is read by hand: bare keys and single quotes are the norm, and -`json.loads` recovers about a third. A command built from a variable falls back to the -whole script as the command text, which still matches as text. Both outputs are read so a -failed call is not recorded as a success. +`agent_thread_id` = the child, and its `wait` names no child at all. `_tool_item_root` +maps a `started` item onto the v1 spawn shape where items enter the turn state, so one +path registers both, and registers a child once: a repeated spawn item would otherwise +mine the rollout twice and double the child's rows and tokens. The other kinds +(`completed`, `interrupted`, `interacted`) are notices about a spawned child, dropped +there, so they neither become rows nor open a parent model call. + +A child's tool calls come in two shapes. A direct call is a function call, and its output +header carries the exit code. A code-mode call is a custom tool `exec` whose input is a +JavaScript script, often several shell calls in a `Promise.all`. Its output tells only +whether the SCRIPT ran to the end: `tools.exec_command` hands a non-zero exit code back to +the script and does not throw, so `Script completed` is not a command result. The script +source is not parsed — a command built from a variable or a loop defeats any parser, and +the source holds no exit code. Instead, while a script runs, Codex writes an +`item_completed` record per shell command (`CommandExecution`: argv, exit code, output) +and per patch (`FileChange`), and those records ARE the script's rows, each with its own +result. A script that left no record stays one `exec` row, not a `Bash` row: its +JavaScript is not a shell command. The argv's login-shell wrapper (`/bin/zsh -lc `) +is stripped, so a record's row reads like a direct call's. + +A record names no script, so it goes to the newest script whose source holds its +command. A command can outlive its script: `tools.exec_command` returns at the call's own +`yield_time_ms` with a session id, the script ends, and a later script polls the command +with `tools.write_stdin`. The text match then gives the record to the script that started +the command, so that script is not also left as an `exec` row that repeats the command. +The match ignores backslashes, because the source escapes the backslashes and double +quotes that the command holds; without that, no record of a real 0.156.1 Windows e2e +child matched. A record that matches no source goes to the newest script: dropping it +would lose a command built from a variable, the miss this recovery exists to prevent, +while a wrong guess only moves the row to another generation. A record whose id is a +direct call's is dropped, so a direct call is never counted twice. Seen on real CLI +0.156.1 runs, v1 and v2 children alike: the records sit between the script call and its +output, before the generation's `token_count`, and a 45-second command was polled as +described. CLIs through 0.147 write no `CommandExecution` records, so there a code-mode +script stays one `exec` row and its commands are not recoverable; 0.156.1 and 0.162 write +them, on Windows too; 0.155, the SDK floor, is not checked. + +A call with no result is `unknown`, never a success, so `require_success` does not credit +it: an interrupt cut it off, it still ran after its yield time, or `wait` stopped it. The +abort output is matched whole, so a command that prints `aborted by user` keeps its exit +code. The `wait` or `write_stdin` call that later returns the result is a row of its own +and does not change the yielded call: the records already carry each command's exit code, +so only a yielded script that ran no command, or a direct `exec_command` that a direct +`write_stdin` polls (no recorded run has one), stays `unknown`. A resuming call stays a +row because a generation that only resumed would otherwise hold no block, and the recovery +drops such a generation with its tokens; so each poll is one row, where the parent stream +records a polled command once. The abort forms come from `abort_message` in +`core/src/tools/parallel.rs` (only `exec_command` gets the wall time). A script yields +after `DEFAULT_EXEC_YIELD_TIME_MS` (10 s); about 8% of the scripts in recorded 0.144 to +0.147 threads yielded. A spawn with `fork_turns` copies the parent's history, tool calls included, into the head of the child's rollout. In a forked rollout (`session_meta` carries `forked_from_id`), parsing starts at the first `thread_settings_applied` with the child's thread id, so the parent's commands are not credited to the child. A non-forked rollout is parsed whole: -that event also appears mid-thread on a settings change. +that event also appears mid-thread on a settings change. A fork without that event is +also parsed whole, with a warning, so a format change that would credit the parent's +commands to the child shows in the logs. The thread-cumulative baseline is the other half. `ThreadTokenUsage.total` counts the whole THREAD, and the Codex thread is created once per task and reused, so by turn N it still diff --git a/docs/agents/CODEX.md b/docs/agents/CODEX.md index 4e86e701..87e23674 100644 --- a/docs/agents/CODEX.md +++ b/docs/agents/CODEX.md @@ -210,6 +210,30 @@ The agent sets up SKILL.md files (Agent Skills open standard) in `.agents/skills 5. Codex CLI auto-discovers skills in `.agents/skills/` 6. Removes the copies when the turn ends, so criteria and run artifacts never see them +### Sub-agents + +A sub-agent runs on its own Codex thread, and its tool calls never reach the parent's +stream. After each turn the agent reads the child's rollout file under +`$CODEX_HOME/sessions` and records the child's calls as telemetry, nested under the +spawning `Agent` row. Spawns from multi-agent v1 (`spawnAgent`) and v2 +(`subAgentActivity`) are both recovered. + +- Each shell command the child ran is one `Bash` row with its own exit code, also when it + runs longer than its script. A code-mode script that runs several commands gives one row + per command. +- A non-zero exit is `result_status: error`. A call that Codex reports with no result is + `result_status: unknown`: an interrupt stopped it, or it still ran when its tool call + returned. `command_executed` with `require_success: true` counts neither. A later `wait` + or `write_stdin` call is a row of its own and does not change that status. +- Each file the child patched is a `Write` row. +- A code-mode `exec` script that ran no command and changed no file (for example, one + that only reads the tool list) is one `exec` row, not a `Bash` row. +- CLI 0.147 and earlier write no command records; 0.156.1 and 0.162 do, and the versions + between them are not checked. On a CLI without records a code-mode script is one `exec` + row, and the commands inside it are not recovered. +- A multi-agent v2 spawn names no model, so the child's generations are priced at the + parent's model. + ### Async Integration The Codex SDK is synchronous. The agent uses `_run_async()` helper to detect and await coroutines, preserving the async interface. diff --git a/docs/agents/HARNESS_PARITY.md b/docs/agents/HARNESS_PARITY.md index dc751c86..829438b9 100644 --- a/docs/agents/HARNESS_PARITY.md +++ b/docs/agents/HARNESS_PARITY.md @@ -531,6 +531,18 @@ what that cell means: measured, but not placeable. On historical codex runs, thi moves ~8 h in aggregate out of Tool exec and into Unaccounted. No built-in agent writes this shape now. +## Sub-agent tool calls + +Only claude-code and codex nest a sub-agent's work under its spawn row +(`parent_tool_use_id`); the other harnesses record no sub-agent path. + +| | claude-code | codex | +|---|---|---| +| spawn row | `Agent`, with the brief in `description` / `prompt` | `Agent` (`operation: spawnagent`), from a v1 `collabAgentToolCall` or a v2 `subAgentActivity` `started` item; on v2, no prompt (the spawn message is encrypted) and no model | +| the child's generations | nested live, with the child's real model | nested after the turn, rebuilt from the child's rollout under `$CODEX_HOME/sessions`, priced at the parent's model on v2 | +| the child's shell commands | one `Bash` row per call, status and `duration_ms` measured live | one `Bash` row per command, with its exit code, rebuilt from the child's rollout (CLI 0.156.1 and later); no `duration_ms`. See [Codex § Sub-agents](CODEX.md#sub-agents) | +| when the rows exist | during the turn, so live criteria see them | after the turn, so live criteria and `stop_early` do not see them | + ## `max_turns` counts model API calls on every harness One turn is one main-thread model API call, the unit Claude Code's `--max-turns` diff --git a/src/coder_eval/agents/codex_agent.py b/src/coder_eval/agents/codex_agent.py index 7ef5a796..4b201081 100644 --- a/src/coder_eval/agents/codex_agent.py +++ b/src/coder_eval/agents/codex_agent.py @@ -14,7 +14,8 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, ClassVar, NamedTuple +from types import SimpleNamespace +from typing import Any, ClassVar, Literal, NamedTuple from urllib.parse import urlparse from coder_eval.agent import Agent, AgentState @@ -117,7 +118,6 @@ "commandExecution": "Bash", "fileChange": "Write", "collabAgentToolCall": "Agent", - "subAgentActivity": "Agent", "mcpToolCall": "Mcp", "dynamicToolCall": "Tool", "webSearch": "WebSearch", @@ -130,7 +130,8 @@ # to recover. Lowercased to match _status_value's normalization. _COLLAB_SPAWN_TOOL = "spawnagent" -# A subAgentActivity `started` item is the spawn; its other kinds are notices. +# Multi-agent v2's sub-agent item: the `started` kind is the spawn, the other kinds +# are notices about a spawned child. # Rationale: .claude/notes/agents.md § Codex rollout rebuild _SUB_AGENT_ACTIVITY_ITEM = "subAgentActivity" _SUB_AGENT_SPAWN_KIND = "started" @@ -143,20 +144,44 @@ "shell": "Bash", "local_shell": "Bash", "shell_command": "Bash", - # A script: _exec_shell_commands reads the commands out of it. - "exec": "Bash", "apply_patch": "Write", "spawn_agent": "Agent", "wait_agent": "Agent", } -# The shell call inside an exec script; the object literal follows. cli 0.140-0.147 -# wrote `tools.shell_command({command: …})`, cli 0.162 writes -# `tools.exec_command({"cmd": …})` — both names and both keys are accepted. -_EXEC_SHELL_CALL = re.compile(r"tools\.(?:shell_command|exec_command)\s*\(") -# The command key of that literal, quoted or bare (the script is JavaScript). -_EXEC_COMMAND_KEY = re.compile(r"""(?:["'](?:command|cmd)["']|\b(?:command|cmd))\s*:\s*""") -_JS_ESCAPES = {"n": "\n", "t": "\t", "r": "\r", "0": "\0", "b": "\b", "f": "\f", "v": "\v"} +# A code-mode `exec` script call, and the first line of its output when the script +# ran to the end. Its commands come from the rollout's `item_completed` records. +# Rationale: .claude/notes/agents.md § Codex rollout rebuild +_EXEC_TOOL = "exec" +_EXEC_SCRIPT_COMPLETED = "Script completed" +# The first line of a script that `wait` stopped before it ended: it has no result. +_EXEC_SCRIPT_TERMINATED = "Script terminated" +_ITEM_COMPLETED_EVENT = "item_completed" +_COMMAND_RECORD_ITEM = "CommandExecution" +_FILE_CHANGE_RECORD_ITEM = "FileChange" +# The result line in the header of a direct shell call's output: `shell_command` +# writes `Exit code: N`, `exec_command` and `write_stdin` write `Process exited with +# code N`, or `Process running with session ID N` while the command still runs. +_SHELL_RESULT_LINE = re.compile( + r"^(?:(?:Exit code: |Process exited with code )(?P-?\d+)|Process running with session ID \d+)", + re.MULTILINE, +) +# The first line of a script that still runs after its yield time. +_EXEC_SCRIPT_YIELDED = "Script running with cell ID" +_WAIT_TOOL = "wait" +# The whole output Codex writes for a call that an interrupt cut off: a direct +# `exec_command` gets its wall time, any other tool the seconds it ran. +_ABORTED_CALL = re.compile(r"(?:Wall time: [\d.]+ seconds\n)?aborted by user(?: after [\d.]+s)?") +# A direct call that did not run at all: `exec_command failed: ...` when the process +# could not start, `write_stdin failed: Unknown process id 7`. No result line and no +# output body, unlike a patch's `Success. Updated ...`. +_CALL_FAILED = re.compile(r"\w+ failed:") +# A shell wrapper's argv: the shell (sh/bash/zsh/pwsh, powershell, cmd), then the +# flag that precedes the command (a `-c` cluster such as `-lc`, `-Command`, `/c`). +_SHELL_PROGRAM = re.compile(r"(?:.*[\\/])?(?:[a-z]*sh|powershell|cmd)(?:\.exe)?", re.IGNORECASE) +_SHELL_SCRIPT_FLAG = re.compile(r"-[a-z]*c|-command|/c", re.IGNORECASE) + +_ResultStatus = Literal["success", "error", "unknown"] # Rollout ResponseItem payload types that are sub-agent tool CALLS and their # matching OUTPUT type (keyed by `call_id`). Anything not listed is skipped. @@ -231,88 +256,51 @@ def _status_value(status: Any) -> str: return str(value).lower() if value is not None else "" -def _js_string_at(text: str, i: int) -> tuple[str, int] | None: - """Read the JavaScript string literal starting at ``text[i]``. +def _record_command_text(argv: Any) -> str: + """The command a CommandExecution record ran, without the shell wrapper Codex adds. - Returns (decoded value, index after the closing quote), or None when ``text[i]`` - is not a quote or the literal never closes. Single, double and backtick quotes; - backslash escapes decoded (``\\n``, ``\\"``, ``\\\\``, ``\\uXXXX``); a ``${}`` - inside a template literal stays verbatim. + ``["/bin/zsh", "-lc", "pytest -q"]`` gives ``pytest -q``; an argv that is not a + shell wrapper is joined with shell quoting. """ - if i >= len(text) or text[i] not in "\"'`": - return None - quote = text[i] - out: list[str] = [] - j = i + 1 - while j < len(text): - ch = text[j] - if ch == "\\" and j + 1 < len(text): - esc = text[j + 1] - if esc == "u" and j + 5 < len(text): - with contextlib.suppress(ValueError): - out.append(chr(int(text[j + 2 : j + 6], 16))) - j += 6 - continue - out.append(_JS_ESCAPES.get(esc, esc)) - j += 2 - continue - if ch == quote: - return "".join(out), j + 1 - out.append(ch) - j += 1 - return None + if isinstance(argv, str): + return argv + if not isinstance(argv, list): + return "" + parts = [str(a) for a in argv] + if len(parts) >= 3 and _SHELL_PROGRAM.fullmatch(parts[0]) and _SHELL_SCRIPT_FLAG.fullmatch(parts[-2]): + return parts[-1] + return shlex.join(parts) -def _matching_brace(text: str, start: int) -> int: - """Index just past the ``}`` matching ``text[start] == "{"``, skipping strings. +def _file_change_outcome(paths: list[str], status: str) -> tuple[_ResultStatus, str]: + """(result_status, result_summary) of a Codex file change; a failed or declined patch wrote nothing.""" + if status in _FILE_CHANGE_FAILURE_STATUSES: + return "error", f"apply_patch {status}: {len(paths)} file(s) not written" + return "success", f"{len(paths)} file(s) changed" - Falls back to ``len(text)`` for an unbalanced literal, so a truncated script - still yields whatever it holds. - """ - depth = 0 - i = start - while i < len(text): - ch = text[i] - if ch in "\"'`": - read = _js_string_at(text, i) - if read is None: - return len(text) - i = read[1] - continue - if ch == "{": - depth += 1 - elif ch == "}": - depth -= 1 - if depth == 0: - return i + 1 - i += 1 - return len(text) - - -def _exec_shell_commands(script: str) -> list[str]: - """Every literal command an exec script hands to the shell tool. - - A command built from a variable is skipped. - Rationale: .claude/notes/agents.md § Codex rollout rebuild + +def _exec_script_status(text: str) -> _ResultStatus: + """An ``exec`` script's status from the first line of its output, or of a ``wait`` output.""" + output = text.lstrip() + if output.startswith(_EXEC_SCRIPT_COMPLETED): + return "success" + if output.startswith((_EXEC_SCRIPT_TERMINATED, _EXEC_SCRIPT_YIELDED)): + return "unknown" + return "error" + + +def _shell_output_status(text: str) -> _ResultStatus: + """A direct call's status from the result line in its output header. + + Without one the call succeeded, unless the output says the call itself failed. """ - commands: list[str] = [] - for call in _EXEC_SHELL_CALL.finditer(script): - brace = script.find("{", call.end()) - if brace < 0: - continue - body = script[brace : _matching_brace(script, brace)] - key = _EXEC_COMMAND_KEY.search(body) - if key is None: - continue - read = _js_string_at(body, key.end()) - if read is not None and read[0]: - commands.append(read[0]) - return commands - - -def _is_subagent_lifecycle_notice(root: Any, root_type: str | None) -> bool: - """True for a subAgentActivity item other than its `started` spawn.""" - return root_type == _SUB_AGENT_ACTIVITY_ITEM and _status_value(getattr(root, "kind", "")) != _SUB_AGENT_SPAWN_KIND + header, has_body, _ = text.partition("\nOutput:") + result = _SHELL_RESULT_LINE.search(header) if has_body else None + if result is None: + return "error" if _CALL_FAILED.match(text.lstrip()) else "success" + if result["exit_code"] is None: + return "unknown" + return "success" if result["exit_code"] == "0" else "error" def _fresh_input_tokens(raw_input: int, cached: int) -> int: @@ -420,6 +408,30 @@ def _get_item_root(notification: Any) -> Any: return getattr(item, "root", None) +def _tool_item_root(notification: Any) -> Any: + """The item root as the turn state handles it, with multi-agent v2 mapped onto v1. + + A ``subAgentActivity`` ``started`` item becomes the v1 ``collabAgentToolCall`` + spawn of its child, under the same id; its other kinds are notices and give + None. Every other root passes through. + Rationale: .claude/notes/agents.md § Codex rollout rebuild + """ + root = _get_item_root(notification) + if getattr(root, "type", None) != _SUB_AGENT_ACTIVITY_ITEM: + return root + if _status_value(getattr(root, "kind", "")) != _SUB_AGENT_SPAWN_KIND: + return None + child = getattr(root, "agent_thread_id", None) + return SimpleNamespace( + type="collabAgentToolCall", + id=getattr(root, "id", None), + tool=_COLLAB_SPAWN_TOOL, + status="completed", + receiver_thread_ids=[str(child)] if child else [], + agent_path=getattr(root, "agent_path", None), + ) + + class _CodexTurnState: """Per-turn mutable scratch state for one ``CodexAgent.communicate`` call. @@ -694,7 +706,7 @@ def on_error(self, notification: Any) -> None: def on_item_started(self, notification: Any) -> None: """Emit ToolStartEvent + record the tool_use block for every tool-like item.""" - root = _get_item_root(notification) + root = _tool_item_root(notification) if root is None: return # Record the start time for every item kind so flushed messages get real timing. @@ -703,8 +715,6 @@ def on_item_started(self, notification: Any) -> None: if item_id is not None and started_at_ms is not None: self.start_ms_by_id[item_id] = started_at_ms root_type = getattr(root, "type", None) - if _is_subagent_lifecycle_notice(root, root_type): - return if not self.in_api_call and root_type not in _NON_MODEL_ITEM_TYPES: self._open_api_call() # Any item that isn't transcript content is a tool call (generic capture). @@ -735,13 +745,11 @@ def on_item_started(self, notification: Any) -> None: def on_item_completed(self, notification: Any) -> None: """Emit ToolEndEvent + capture telemetry / sub-agents / transcript blocks.""" - root = _get_item_root(notification) + root = _tool_item_root(notification) if root is None: return completed_ms = getattr(notification.payload, "completed_at_ms", None) root_type = getattr(root, "type", None) - if _is_subagent_lifecycle_notice(root, root_type): - return if root_type is not None and root_type not in _CONTENT_ITEM_TYPES: tool_id = getattr(root, "id", None) or f"{root_type}_{self.next_sequence}" seq = self.seq_by_id.get(tool_id, self.next_sequence) @@ -794,13 +802,6 @@ def on_item_completed(self, notification: Any) -> None: self.spawned_children, self.collab_results, ) - elif root_type == _SUB_AGENT_ACTIVITY_ITEM: - self._agent._handle_subagent_spawn( - root, - tool_id, - self.collab_spawn_by_thread, - self.spawned_children, - ) elif root_type == "reasoning": # OpenAI never returns raw CoT, so a text-less item becomes a @@ -1863,13 +1864,6 @@ def _tool_parameters(self, root: Any, root_type: str | None) -> dict[str, Any]: params["model"] = model if prompt := getattr(root, "prompt", None): params["prompt"] = prompt - return params - if root_type == _SUB_AGENT_ACTIVITY_ITEM: - # Only the `started` kind reaches here; it is the spawn call. - params = { - "operation": _COLLAB_SPAWN_TOOL, - "agent_thread_id": str(getattr(root, "agent_thread_id", "") or ""), - } if agent_path := getattr(root, "agent_path", None): params["agent_path"] = str(agent_path) return params @@ -1935,10 +1929,6 @@ def _extract_generic_telemetry( """ try: status_str = _status_value(getattr(root, "status", "") or "") - if root_type == _SUB_AGENT_ACTIVITY_ITEM and not status_str: - # Codex emits the spawn activity only once the child thread - # exists, so a status-less item is a successful spawn. - status_str = "completed" err = getattr(root, "error", None) success = getattr(root, "success", None) is_error = bool(err) or success is False or status_str in _FILE_CHANGE_FAILURE_STATUSES @@ -1971,9 +1961,6 @@ def _summarize_tool_item(root: Any, root_type: str | None) -> str: states = getattr(root, "agents_states", None) or {} messages = [s.message for s in states.values() if getattr(s, "message", None)] return f"collab {op}: {'; '.join(messages)[:200]}" if messages else f"collab {op}" - if root_type == _SUB_AGENT_ACTIVITY_ITEM: - target = getattr(root, "agent_path", "") or getattr(root, "agent_thread_id", "") - return f"collab {_COLLAB_SPAWN_TOOL}: {target}" if root_type == "webSearch": return f"query: {getattr(root, 'query', '')}" if root_type in ("mcpToolCall", "dynamicToolCall"): @@ -1996,7 +1983,8 @@ def _handle_collab_completion( 1. SPAWN: remember which Agent call owns each spawned child thread, and the spawned model. Follow-up ``wait``/messaging calls reuse the same thread - and are NOT new sub-agents. + and are NOT new sub-agents. A child is registered once, so a repeated + spawn item cannot mine its rollout twice. 2. RESULT: stash the child's returned message as a FALLBACK, used only when the child's rollout cannot be found later. @@ -2006,36 +1994,18 @@ def _handle_collab_completion( receivers = getattr(root, "receiver_thread_ids", None) or [] if tool == _COLLAB_SPAWN_TOOL: model = getattr(root, "model", None) or self._effective_model() or None - for thread_id in receivers: + for thread_id in map(str, receivers): + if thread_id in spawn_by_thread: + continue spawn_by_thread[thread_id] = tool_id - spawned_children.append((str(thread_id), tool_id, model)) + spawned_children.append((thread_id, tool_id, model)) states = getattr(root, "agents_states", None) or {} for thread_id, state in states.items(): message = getattr(state, "message", None) - if message and thread_id in spawn_by_thread: + if message and str(thread_id) in spawn_by_thread: collab_results[str(thread_id)] = str(message) - def _handle_subagent_spawn( - self, - root: Any, - tool_id: str, - spawn_by_thread: dict[str, str], - spawned_children: list[tuple[str, str, str | None]], - ) -> None: - """Register the child a `started` subAgentActivity item announces, under ``tool_id``. - - Idempotent per thread. Rationale: .claude/notes/agents.md § Codex rollout rebuild - """ - thread_id = getattr(root, "agent_thread_id", None) - if not thread_id: - return - thread_id = str(thread_id) - if thread_id in spawn_by_thread: - return - spawn_by_thread[thread_id] = tool_id - spawned_children.append((thread_id, tool_id, None)) - async def _recover_subagent_tool_calls( self, spawned_children: list[tuple[str, str, str | None]], @@ -2134,38 +2104,39 @@ def _parse_rollout_generations(cls, path: Path) -> list[dict[str, Any]]: A ``token_count`` event marks each generation boundary. Tool CALLS are paired with their OUTPUT by ``call_id``, since the output can be emitted a - generation later. Trailing items with no closing ``token_count`` flush as a - final token-less generation; corrupt lines are skipped. + generation later. A code-mode ``exec`` script's rows are the + ``item_completed`` records Codex writes for it, one per shell command or file + change, in the script's generation; a script that left no record is one + ``exec`` row. Trailing items with no closing ``token_count`` flush as a final + token-less generation; corrupt lines are skipped. + Rationale: .claude/notes/agents.md § Codex rollout rebuild """ - objs: list[dict[str, Any]] = [] - for raw in path.read_text(encoding="utf-8").splitlines(): - raw = raw.strip() - if not raw: - continue - try: - objs.append(json.loads(raw)) - except json.JSONDecodeError: - continue + objs = cls._rollout_objects(path) objs = objs[cls._fork_boundary(objs) :] # Pass 1: tool OUTPUTs by call_id. - outputs: dict[str, tuple[str, bool]] = {} + outputs: dict[str, dict[str, Any]] = {} for obj in objs: if obj.get("type") == "response_item": p = obj.get("payload") or {} if p.get("type") in _ROLLOUT_TOOL_OUTPUT_TYPES: - outputs[str(p.get("call_id") or "")] = cls._subagent_output(p) + outputs[str(p.get("call_id") or "")] = p # Pass 2: walk into generations. gens: list[dict[str, Any]] = [] cur: list[dict[str, Any]] = [] n_calls = 0 + direct_call_ids: set[str] = set() + # Every exec script, and the records each one wrote, by its call id: a + # script's records replace its row. + scripts: list[dict[str, Any]] = [] + records: dict[str, list[dict[str, Any]]] = {} def close(tokens: tuple[int, int, int, int] | None) -> None: nonlocal cur if not cur and tokens is None: return - gens.append({"tokens": tokens, "items": cur, "tools": [it["call"] for it in cur if it["kind"] == "tool"]}) + gens.append({"tokens": tokens, "items": cur}) cur = [] for obj in objs: @@ -2176,23 +2147,35 @@ def close(tokens: tuple[int, int, int, int] | None) -> None: if pt in _ROLLOUT_TOOL_CALL_TYPES: call_id = str(p.get("call_id") or p.get("id") or f"call_{n_calls}") n_calls += 1 - summary, is_error = outputs.get(call_id, ("", False)) - cur.append( - { - "kind": "tool", - "call": { - "call_id": call_id, - "tool_name": cls._subagent_tool_name(p), - "parameters": cls._subagent_parameters(p), - "result_summary": summary, - "is_error": is_error, - }, - } + name = p.get("name") + summary, status = cls._subagent_output( + outputs.get(call_id), exec_script=name in (_EXEC_TOOL, _WAIT_TOOL) ) + call = { + "call_id": call_id, + "tool_name": cls._subagent_tool_name(p), + "parameters": cls._subagent_parameters(p), + "result_summary": summary, + "status": status, + } + if name == _EXEC_TOOL: + scripts.append(call) + else: + direct_call_ids.add(call_id) + cur.append({"kind": "tool", "call": call}) elif pt == "message" and p.get("role") == "assistant": text = cls._message_text(p.get("content")) if text: cur.append({"kind": "text", "text": text}) + elif t == "event_msg" and p.get("type") == _ITEM_COMPLETED_EVENT: + record = cls._record_call(p.get("item"), f"call_{n_calls}") + if record is None or record["call_id"] in direct_call_ids: + continue + owner = cls._record_owner(record, scripts) + if owner is None: + continue + n_calls += 1 + records.setdefault(owner["call_id"], []).append(record) elif t == "event_msg" and p.get("type") == "token_count": last = (p.get("info") or {}).get("last_token_usage") or {} close( @@ -2204,8 +2187,33 @@ def close(tokens: tuple[int, int, int, int] | None) -> None: ) ) close(None) + for gen in gens: + gen["items"] = [row for item in gen["items"] for row in cls._script_rows(item, records)] + gen["tools"] = [item["call"] for item in gen["items"] if item["kind"] == "tool"] return gens + @staticmethod + def _script_rows(item: dict[str, Any], records: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: + """An item's rows: a script's records in its place when it has any, else the item itself.""" + own = records.get(item["call"]["call_id"]) if item["kind"] == "tool" else None + return [{"kind": "tool", "call": record} for record in own] if own else [item] + + @staticmethod + def _rollout_objects(path: Path) -> list[dict[str, Any]]: + """The JSON-object lines of a rollout file; blank, corrupt and non-object lines are skipped.""" + objs: list[dict[str, Any]] = [] + for raw in path.read_text(encoding="utf-8").splitlines(): + raw = raw.strip() + if not raw: + continue + try: + obj = json.loads(raw) + except json.JSONDecodeError: + continue + if isinstance(obj, dict): + objs.append(obj) + return objs + @staticmethod def _fork_boundary(objs: list[dict[str, Any]]) -> int: """Index where the child's own history starts. @@ -2229,6 +2237,7 @@ def _fork_boundary(objs: list[dict[str, Any]]) -> int: and p.get("thread_id") == own ): return i + logger.warning("CodexAgent: forked rollout %s has no thread_settings_applied of its own; parsed whole", own) return 0 @staticmethod @@ -2256,13 +2265,8 @@ def _subagent_parameters(payload: dict[str, Any]) -> dict[str, Any]: """Best-effort parameters for a rollout tool-call ResponseItem. Shell-style calls are normalized to ``{"command": ...}`` so the transcript - renders the command line; everything else passes through. An ``exec`` - script yields its literal commands, one per line, or itself when it has none. + renders the command line; everything else passes through. """ - if payload.get("name") == "exec" and isinstance(payload.get("input"), str): - script = payload["input"] - commands = _exec_shell_commands(script) - return {"command": "\n".join(commands) if commands else script} args = payload.get("arguments") if isinstance(args, str) and args: with contextlib.suppress(json.JSONDecodeError): @@ -2285,27 +2289,85 @@ def _subagent_parameters(payload: dict[str, Any]) -> dict[str, Any]: return {} @staticmethod - def _subagent_output(payload: dict[str, Any]) -> tuple[str, bool]: - """(result_summary, is_error) for a rollout tool-output ResponseItem. + def _subagent_output(payload: dict[str, Any] | None, *, exec_script: bool = False) -> tuple[str, _ResultStatus]: + """(result_summary, result_status) for a rollout tool-output ResponseItem. - An exec output is a list of content parts starting ``Script completed`` or - ``Script failed``; a shell output is a string starting ``Exit code: N``. + An ``exec`` script succeeded only when its output starts ``Script completed``. + A shell call's status is the result line in the header of its output. A call + with no result is ``unknown``: its output never arrived, an interrupt cut it + off, or it was stopped or still runs. """ + if payload is None: + return "", "unknown" output = payload.get("output") - is_error = False if isinstance(output, list): text = "".join(str(part.get("text", "")) if isinstance(part, dict) else str(part) for part in output) - is_error = text.lstrip().startswith("Script failed") - return text, is_error - if isinstance(output, str) and (exit_code := re.match(r"Exit code: (\d+)", output)): - return output, exit_code.group(1) != "0" - if isinstance(output, dict): - if output.get("success") is False: - is_error = True - text = output.get("content") or output.get("output") or json.dumps(output) + elif isinstance(output, dict): + text = str(output.get("content") or output.get("output") or json.dumps(output)) else: text = "" if output is None else str(output) - return str(text), is_error + if _ABORTED_CALL.fullmatch(text.strip()): + return text, "unknown" + if exec_script: + return text, _exec_script_status(text) + if isinstance(output, dict) and output.get("success") is False: + return text, "error" + return text, _shell_output_status(text) + + @staticmethod + def _record_call(item: Any, fallback_id: str) -> dict[str, Any] | None: + """The recovered call for an ``item_completed`` record in a rollout. + + A ``CommandExecution`` is a Bash row with its own exit code, a ``FileChange`` + a Write row; any other item gives None. + """ + if not isinstance(item, dict): + return None + call_id = str(item.get("id") or fallback_id) + if item.get("type") == _COMMAND_RECORD_ITEM: + exit_code = item.get("exit_code") + return { + "call_id": call_id, + "tool_name": "Bash", + "parameters": {"command": _record_command_text(item.get("command"))}, + "result_summary": str(item.get("aggregated_output") or ""), + "status": "success" if exit_code == 0 else "error" if exit_code is not None else "unknown", + } + if item.get("type") == _FILE_CHANGE_RECORD_ITEM: + changes = item.get("changes") + paths = [str(path) for path in changes] if isinstance(changes, dict) else [] + status, summary = _file_change_outcome(paths, _status_value(item.get("status"))) + return { + "call_id": call_id, + "tool_name": "Write", + "parameters": {"paths": paths}, + "result_summary": summary, + "status": status, + } + return None + + @staticmethod + def _record_owner(record: dict[str, Any], scripts: list[dict[str, Any]]) -> dict[str, Any] | None: + """The exec script a record came from. + + The newest script whose source holds the record's command, compared with + backslashes removed; else the newest script. None only when the rollout holds + no script. + Rationale: .claude/notes/agents.md § Codex rollout rebuild + """ + command = record["parameters"].get("command") + text = str(command or "").replace("\\", "") + sources = ((s, str(s["parameters"].get("input", "")).replace("\\", "")) for s in reversed(scripts)) + owner = next((s for s, source in sources if text and text in source), None) + if owner is None and scripts: + owner = scripts[-1] + logger.debug( + "CodexAgent: %s record %r matches no script source; attributed to script %s", + record["tool_name"], + command or record["parameters"], + owner["call_id"], + ) + return owner def _subagent_generation_blocks( self, gen: dict[str, Any], thread_id: str @@ -2323,7 +2385,9 @@ def _subagent_generation_blocks( call = item["call"] tool_id = f"sub:{thread_id}:{call['call_id']}" blocks.append( - ContentBlock(block_type="tool_use", sequence=seq, tool_use_id=tool_id, is_error=call["is_error"]) + ContentBlock( + block_type="tool_use", sequence=seq, tool_use_id=tool_id, is_error=call["status"] == "error" + ) ) telemetries.append( CommandTelemetry( @@ -2331,7 +2395,7 @@ def _subagent_generation_blocks( tool_id=tool_id, timestamp=datetime.now(), parameters=call["parameters"], - result_status="error" if call["is_error"] else "success", + result_status=call["status"], result_summary=call["result_summary"], ) ) @@ -2473,7 +2537,7 @@ def _extract_file_change_telemetry( try: paths = [str(c.path) for c in changes if hasattr(c, "path")] if changes else [] status_str = _status_value(status) - failed = status_str in _FILE_CHANGE_FAILURE_STATUSES + result_status, result_summary = _file_change_outcome(paths, status_str) timing = _item_timing(started_ms, completed_ms, None) return CommandTelemetry( tool_name="Write", @@ -2483,13 +2547,9 @@ def _extract_file_change_telemetry( execution_completed_at=timing.execution_completed_at, duration_ms=timing.duration_ms, parameters={"paths": paths}, - result_status="error" if failed else "success", - result_summary=( - f"{len(paths)} file(s) changed" - if not failed - else f"apply_patch {status_str}: {len(paths)} file(s) not written" - ), - error_message=f"apply_patch {status_str}" if failed else None, + result_status=result_status, + result_summary=result_summary, + error_message=f"apply_patch {status_str}" if result_status == "error" else None, result_data=None, sequence_number=sequence, ) diff --git a/tests/_fixtures/codex_rollouts.py b/tests/_fixtures/codex_rollouts.py new file mode 100644 index 00000000..8a4574ed --- /dev/null +++ b/tests/_fixtures/codex_rollouts.py @@ -0,0 +1,113 @@ +"""Builders for Codex sub-agent rollout files, shared by the SDK and SDK-free Codex tests. + +A rollout is the JSONL a Codex thread writes under ``/sessions``; the +agent reads a child's rollout to recover what the child ran. The record builders +use the field names of a real CLI 0.156.1 rollout. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +_LINE_TYPES = {"response_item", "event_msg", "session_meta", "turn_context"} + + +def write_child_rollout(home: Path, thread_id: str, items: list[dict[str, Any]]) -> Path: + """Write a child-thread rollout JSONL under ``/sessions`` and return its path. + + The thread id is embedded in the filename verbatim (that's how the agent + locates it). An entry with a top-level line ``type`` (``event_msg``, + ``session_meta``, …) is written verbatim; any other entry is a raw ResponseItem + payload and is wrapped as a ``response_item`` line. + """ + sessions = home / "sessions" / "2026" / "06" / "05" + sessions.mkdir(parents=True, exist_ok=True) + path = sessions / f"rollout-2026-06-05T14-22-36-{thread_id}.jsonl" + lines = [ + json.dumps(it if it.get("type") in _LINE_TYPES else {"type": "response_item", "payload": it}) for it in items + ] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return path + + +def token_count_event(*, inp: int, cached: int, out: int, tot_in: int, tot_cached: int, tot_out: int) -> dict: + """A child-rollout ``token_count`` event_msg (a generation boundary).""" + return { + "type": "event_msg", + "payload": { + "type": "token_count", + "info": { + "last_token_usage": {"input_tokens": inp, "cached_input_tokens": cached, "output_tokens": out}, + "total_token_usage": { + "input_tokens": tot_in, + "cached_input_tokens": tot_cached, + "output_tokens": tot_out, + }, + }, + }, + } + + +def exec_call(call_id: str, script: str) -> dict[str, Any]: + """A code-mode ``exec`` custom tool call.""" + return {"type": "custom_tool_call", "name": "exec", "call_id": call_id, "input": script} + + +def exec_output(call_id: str, printed: str = "", *, head: str = "Script completed") -> dict[str, Any]: + """An ``exec`` call's output: content parts headed by the script's status line, such as ``Script failed``.""" + return { + "type": "custom_tool_call_output", + "call_id": call_id, + "output": [ + {"type": "input_text", "text": f"{head}\nWall time 0.1 seconds\nOutput:\n"}, + {"type": "input_text", "text": printed}, + ], + } + + +def wait_call(call_id: str, cell_id: str) -> dict[str, Any]: + """A code-mode ``wait`` call that resumes the script that yielded this cell id.""" + return {"type": "function_call", "name": "wait", "call_id": call_id, "arguments": json.dumps({"cell_id": cell_id})} + + +def wait_output(call_id: str, printed: str = "", *, head: str = "Script completed") -> dict[str, Any]: + """A ``wait`` call's output: headed by the resumed script's status line, as an ``exec`` output is.""" + return {**exec_output(call_id, printed, head=head), "type": "function_call_output"} + + +def command_record(command: str, *, exit_code: int | None, item_id: str, output: str = "") -> dict[str, Any]: + """The ``item_completed`` record of one shell command a code-mode script ran.""" + return { + "type": "event_msg", + "payload": { + "type": "item_completed", + "item": { + "type": "CommandExecution", + "id": item_id, + "command": ["/bin/zsh", "-lc", command], + "source": "unified_exec_startup", + "status": "completed" if exit_code == 0 else "failed", + "aggregated_output": output, + "exit_code": exit_code, + }, + }, + } + + +def file_change_record(path: str, *, item_id: str, status: str = "completed") -> dict[str, Any]: + """The ``item_completed`` record of a file a code-mode script patched.""" + return { + "type": "event_msg", + "payload": { + "type": "item_completed", + "item": { + "type": "FileChange", + "id": item_id, + "changes": {path: {"type": "add", "content": "hi\n"}}, + "status": status, + }, + }, + } diff --git a/tests/test_codex_agent.py b/tests/test_codex_agent.py index 8aaaac49..3aec31da 100644 --- a/tests/test_codex_agent.py +++ b/tests/test_codex_agent.py @@ -500,10 +500,22 @@ def test_get_state_returns_current_state(): import time # noqa: E402 from pathlib import Path # noqa: E402 from types import SimpleNamespace # noqa: E402 +from typing import Any # noqa: E402 -from openai_codex.generated.v2_all import Turn, TurnCompletedNotification # noqa: E402 +from openai_codex.generated.v2_all import ( # noqa: E402 + SubAgentActivityThreadItem, + Turn, + TurnCompletedNotification, +) from coder_eval.errors import AgentCrashError, TurnTimeoutError # noqa: E402 +from tests._fixtures.codex_rollouts import ( # noqa: E402 + command_record, + exec_call, + exec_output, + token_count_event, + write_child_rollout, +) def _item_notification( @@ -1047,13 +1059,14 @@ def _collab_call( prompt: str | None = None, result: str | None = None, status: str = "completed", - child_thread: str = "thread_child", + child_thread: str | None = "thread_child", ) -> SimpleNamespace: """A collabAgentToolCall item root (Codex's native multi-agent tool). ``tool`` is the collab operation value ("spawnAgent" or "wait"); ``result`` becomes a spawned agent's returned message (under agents_states). The spawn - and the wait share ``child_thread`` so the result nests under the spawn.""" + and the wait share ``child_thread`` so the result nests under the spawn; a + multi-agent v2 wait names no child (``child_thread=None``).""" states = {} if result is not None: states[child_thread] = SimpleNamespace(message=result, status="completed") @@ -1064,142 +1077,24 @@ def _collab_call( model=model, prompt=prompt, status=status, - receiver_thread_ids=[child_thread], + receiver_thread_ids=[child_thread] if child_thread else [], agents_states=states, ) -class TestExecScriptCommandExtraction: - """Shell commands are read out of an `exec` script so recovered sub-agent rows are Bash rows.""" - - def test_json_quoted_keys(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = ( - 'const r = await tools.shell_command({"command":"git status --short --branch",' - '"workdir":"C:\\\\w","timeout_ms":10000}); text(r);' - ) - assert _exec_shell_commands(script) == ["git status --short --branch"] - - def test_bare_keys_single_quotes_and_escapes(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = ( - 'const a = await tools.shell_command({command:\'pytest --rootdir "C:\\\\p" ' - '-q tests\',workdir:"C:\\\\repo"}); text(a);' - ) - assert _exec_shell_commands(script) == ['pytest --rootdir "C:\\p" -q tests'] - - def test_promise_all_yields_every_command_in_order(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = ( - "const r = await Promise.all([\n" - ' tools.shell_command({command:"git branch --show-current",workdir:"C:\\\\w"}),\n' - ' tools.shell_command({command:"git status --short",workdir:"C:\\\\w"}),\n' - "]);" - ) - assert _exec_shell_commands(script) == ["git branch --show-current", "git status --short"] - - def test_newline_escape_and_template_literal(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = 'await tools.shell_command({command:`echo "a"\\nls -la`})' - assert _exec_shell_commands(script) == ['echo "a"\nls -la'] - - def test_variable_command_is_not_a_literal(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = 'const cmd = "npm run build"; const r = await tools.shell_command({command: cmd});' - assert _exec_shell_commands(script) == [] - - def test_other_tools_are_ignored(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = ( - 'await tools.update_plan({plan:[{step:"x",status:"in_progress"}]}); await tools.view_image({path:"a.png"});' - ) - assert _exec_shell_commands(script) == [] - - def test_exec_command_with_cmd_key(self): - from coder_eval.agents.codex_agent import _exec_shell_commands - - script = ( - 'const r = await tools.exec_command({"cmd":"git log --oneline -1","workdir":"C:\\\\w",' - '"yield_time_ms":10000,"max_output_tokens":2000});\ntext(r.output);\n' - ) - assert _exec_shell_commands(script) == ["git log --oneline -1"] - - def test_exec_output_parts_are_joined_and_a_failed_script_is_an_error(self): - ok = { - "type": "custom_tool_call_output", - "call_id": "c1", - "output": [ - {"type": "input_text", "text": "Script completed\nWall time 1.0 seconds\nOutput:\n"}, - {"type": "input_text", "text": "ok\n"}, - ], - } - failed = { - "type": "custom_tool_call_output", - "call_id": "c2", - "output": [ - {"type": "input_text", "text": "Script failed\nWall time 1.6 seconds\nOutput:\n"}, - {"type": "input_text", "text": "boom\n"}, - ], - } - text, is_error = CodexAgent._subagent_output(ok) - assert "ok" in text - assert not is_error - text, is_error = CodexAgent._subagent_output(failed) - assert "boom" in text - assert is_error - - def test_v1_exit_code_output_marks_non_zero_as_error(self): - assert CodexAgent._subagent_output({"output": "Exit code: 0\nWall time: 1 seconds\nOutput:\nok\n"}) == ( - "Exit code: 0\nWall time: 1 seconds\nOutput:\nok\n", - False, - ) - assert CodexAgent._subagent_output({"output": "Exit code: 1\nOutput:\nboom\n"})[1] is True - assert CodexAgent._subagent_output({"output": "5050\n"}) == ("5050\n", False) - - def test_rollout_exec_call_becomes_a_bash_row_with_the_command(self): - payload = { - "type": "custom_tool_call", - "name": "exec", - "call_id": "call_x", - "input": ('const r = await tools.shell_command({command:"pytest tests/test_api.py -q"}); text(r);'), - } - assert CodexAgent._subagent_tool_name(payload) == "Bash" - assert CodexAgent._subagent_parameters(payload) == {"command": "pytest tests/test_api.py -q"} - - def test_rollout_exec_call_without_a_literal_keeps_the_script_as_command_text(self): - script = 'const cmd = "npm run build"; await tools.shell_command({command: cmd});' - payload = {"type": "custom_tool_call", "name": "exec", "call_id": "call_y", "input": script} - assert CodexAgent._subagent_parameters(payload) == {"command": script} - - def test_v1_shell_command_function_call_is_bash(self): - payload = { - "type": "function_call", - "name": "shell_command", - "call_id": "call_z", - "arguments": '{"command":"git status --short","workdir":"C:\\\\w","timeout_ms":10000}', - } - assert CodexAgent._subagent_tool_name(payload) == "Bash" - assert CodexAgent._subagent_parameters(payload)["command"] == "git status --short" - - def _sub_agent_activity( kind: str, *, item_id: str, child_thread: str, - agent_path: str = "agent-1", -) -> SimpleNamespace: + agent_path: str = "/root/run_tests", +) -> SubAgentActivityThreadItem: """A subAgentActivity item root (Codex multi-agent v2's sub-agent lifecycle item). ``kind`` is "started" (the spawn itself: ``item_id`` is the spawn_agent call id) - or a lifecycle notice ("completed", "interrupted", "interacted").""" - return SimpleNamespace( + or a lifecycle notice ("completed", "interrupted", "interacted"). Built as the + SDK model, so a renamed field fails here and not silently in the agent.""" + return SubAgentActivityThreadItem( type="subAgentActivity", id=item_id, kind=kind, @@ -1208,6 +1103,13 @@ def _sub_agent_activity( ) +def _spawn(shape: str, child: str, *, call_id: str = "call_spawn") -> Any: + """The spawn of ``child``: a v1 collab call or a multi-agent v2 ``started`` item.""" + if shape == "v1": + return _collab_call("spawnAgent", call_id=call_id, model="gpt-5.5", child_thread=child) + return _sub_agent_activity("started", item_id=call_id, child_thread=child) + + class TestCodexCollabSubAgent: """Codex spawns sub-agents via collabAgentToolCall (tool='spawnAgent'), not Claude's Task tool. The agent must surface these as tool calls AND nest the @@ -1292,44 +1194,6 @@ async def test_orphan_tool_started_without_completed_is_closed_unresolved(self, assert "call_orphan" in block_ids -def _write_child_rollout(home, thread_id: str, items: list[dict]) -> None: - """Write a minimal child-thread rollout JSONL under /sessions. - - The thread id is embedded in the filename verbatim (that's how the agent - locates it). Each entry is either a raw ResponseItem payload (function_call, - function_call_output, …) — wrapped as a ``response_item`` line — or, if it - already carries a top-level ``"type"`` (e.g. an ``event_msg`` token_count), - written verbatim so generations can be interleaved with token boundaries.""" - import json as _json - - sessions = home / "sessions" / "2026" / "06" / "05" - sessions.mkdir(parents=True, exist_ok=True) - path = sessions / f"rollout-2026-06-05T14-22-36-{thread_id}.jsonl" - _line_types = {"response_item", "event_msg", "session_meta", "turn_context"} - lines = [ - _json.dumps(it if it.get("type") in _line_types else {"type": "response_item", "payload": it}) for it in items - ] - path.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def _token_count_event(*, inp: int, cached: int, out: int, tot_in: int, tot_cached: int, tot_out: int) -> dict: - """A child-rollout ``token_count`` event_msg (a generation boundary).""" - return { - "type": "event_msg", - "payload": { - "type": "token_count", - "info": { - "last_token_usage": {"input_tokens": inp, "cached_input_tokens": cached, "output_tokens": out}, - "total_token_usage": { - "input_tokens": tot_in, - "cached_input_tokens": tot_cached, - "output_tokens": tot_out, - }, - }, - }, - } - - class TestCodexSubAgentToolRecovery: """A Codex sub-agent runs on a child thread whose tool calls never reach the parent stream and are dropped from `thread.read` (Limited persistence). But @@ -1340,7 +1204,7 @@ class TestCodexSubAgentToolRecovery: async def test_inner_shell_command_recovered_and_nested(self, monkeypatch, tmp_path): monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-aaaa-7000-8000-000000000001" - _write_child_rollout( + write_child_rollout( tmp_path, child, [ @@ -1383,37 +1247,22 @@ async def test_v2_started_activity_is_the_spawn_and_feeds_recovery(self, monkeyp # The wait names no child: only the `started` item can register it. monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-dddd-7000-8000-000000000004" - _write_child_rollout( + script = ( + 'await Promise.all([tools.exec_command({cmd:"pytest tests/test_api.py -q"}),' + ' tools.exec_command({cmd:"ruff check ."})]);' + ) + write_child_rollout( tmp_path, child, [ - { - "type": "custom_tool_call", - "name": "exec", - "call_id": "c_1", - "input": 'await tools.exec_command({"cmd":"pytest tests/test_api.py -q"})', - }, - { - "type": "custom_tool_call_output", - "call_id": "c_1", - "output": [ - {"type": "input_text", "text": "Script completed\nWall time 3.0 seconds\nOutput:\n"}, - {"type": "input_text", "text": "1 passed in 0.31s\n"}, - ], - }, + exec_call("c_1", script), + command_record("pytest tests/test_api.py -q", exit_code=0, item_id="exec-1", output="1 passed\n"), + command_record("ruff check .", exit_code=1, item_id="exec-2", output="E501 line too long\n"), + exec_output("c_1", '[{"exit_code":0},{"exit_code":1}]'), ], ) started = _sub_agent_activity("started", item_id="call_spawn", child_thread=child) - wait = SimpleNamespace( - type="collabAgentToolCall", - id="call_wait", - tool="wait", - model=None, - prompt=None, - status="completed", - receiver_thread_ids=[], - agents_states={}, - ) + wait = _collab_call("wait", call_id="call_wait", child_thread=None) completed = _sub_agent_activity("completed", item_id="subagent-completed-turn_c1", child_thread=child) notifications = [ _item_notification("item/started", started), @@ -1436,46 +1285,51 @@ async def test_v2_started_activity_is_the_spawn_and_feeds_recovery(self, monkeyp assert not [c for c in record.commands if c.tool_name == "subAgentActivity"] assert not [c for c in record.commands if c.tool_id.startswith("subagent-completed")] spawn_row = agent_rows["call_spawn"] - assert spawn_row.parameters["operation"] == "spawnagent" - assert spawn_row.parameters["agent_thread_id"] == child + assert spawn_row.parameters == {"operation": "spawnagent", "agent_path": "/root/run_tests"} assert spawn_row.result_status == "success" - # The child's command is recovered from its rollout and nests under the spawn. + # Each command the child's script ran is a row with its own exit code, under the spawn. bash = [c for c in record.commands if c.tool_name == "Bash"] - assert len(bash) == 1 - assert bash[0].parameters["command"] == "pytest tests/test_api.py -q" - assert bash[0].result_status == "success" + assert [(c.parameters["command"], c.result_status) for c in bash] == [ + ("pytest tests/test_api.py -q", "success"), + ("ruff check .", "error"), + ] assert "1 passed" in (bash[0].result_summary or "") nested = [m for m in record.messages if getattr(m, "parent_tool_use_id", None) == "call_spawn"] - assert any(bash[0].tool_id in m.tool_use_ids for m in nested) + blocks = {b.tool_use_id: b for m in nested for b in m.content_blocks if b.block_type == "tool_use"} + assert [blocks[c.tool_id].is_error for c in bash] == [False, True] assert all(m.model == "parent-model" for m in nested) - async def test_v2_lifecycle_notices_without_a_spawn_record_nothing(self, monkeypatch, tmp_path): - # `completed` / `interrupted` without a `started` are notices about a - # child this turn did not spawn: no tool rows, no sub-agent, no crash. + @pytest.mark.parametrize("delta", [True, False], ids=["with-text", "notices-only"]) + async def test_v2_lifecycle_notices_without_a_spawn_record_nothing(self, monkeypatch, tmp_path, delta): + # `completed` / `interrupted` / `interacted` without a `started` are notices + # about a child this turn did not spawn: no tool rows, no sub-agent, and no + # model call of their own. + from coder_eval.models import AssistantMessage + monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-eeee-7000-8000-000000000005" - completed = _sub_agent_activity("completed", item_id="subagent-completed-turn_x", child_thread=child) - interrupted = _sub_agent_activity("interrupted", item_id="subagent-interrupted-turn_x", child_thread=child) - notifications = [ - _item_notification("item/started", completed), - _item_notification("item/completed", completed), - _item_notification("item/started", interrupted), - _item_notification("item/completed", interrupted), - _delta("done"), - _turn_completed(), - ] + notifications = [] + for kind in ("completed", "interrupted", "interacted"): + notice = _sub_agent_activity(kind, item_id=f"subagent-{kind}-turn_x", child_thread=child) + notifications += [_item_notification("item/started", notice), _item_notification("item/completed", notice)] + notifications += [_delta("done"), _turn_completed()] if delta else [_turn_completed()] agent = _started_agent(parse_agent_config(type=AgentKind.CODEX), notifications) record = await agent.communicate("go") assert record.commands == [] assert not any(getattr(m, "parent_tool_use_id", None) for m in record.messages) + if not delta: + assert not [m for m in record.messages if isinstance(m, AssistantMessage)] - async def test_v2_repeated_started_notice_registers_the_child_once(self, monkeypatch, tmp_path): + @pytest.mark.parametrize("first", ["v1", "v2"]) + async def test_a_second_spawn_of_one_child_does_not_register_it_again(self, monkeypatch, tmp_path, first): + # A repeated `started` item, or a v1 and a v2 spawn of one child, would mine its + # rollout twice and double the child's rows and tokens. monkeypatch.setenv("CODEX_HOME", str(tmp_path)) - child = "019e0000-ffff-7000-8000-000000000006" - _write_child_rollout( + child = "019e0000-abab-7000-8000-00000000000b" + write_child_rollout( tmp_path, child, [ @@ -1483,62 +1337,76 @@ async def test_v2_repeated_started_notice_registers_the_child_once(self, monkeyp {"type": "function_call_output", "call_id": "c_1", "output": "one\n"}, ], ) - started = _sub_agent_activity("started", item_id="call_spawn", child_thread=child) - notifications = [ - _item_notification("item/started", started), - _item_notification("item/completed", started), - _item_notification("item/started", started), - _item_notification("item/completed", started), - _delta("done"), - _turn_completed(), - ] + notifications = [] + for spawn in (_spawn(first, child, call_id="call_1"), _spawn("v2", child, call_id="call_2")): + notifications += [_item_notification("item/started", spawn), _item_notification("item/completed", spawn)] + notifications += [_delta("done"), _turn_completed()] agent = _started_agent(parse_agent_config(type=AgentKind.CODEX), notifications) record = await agent.communicate("go") - # One registration → the rollout is mined once; the command is not doubled. assert len([c for c in record.commands if c.tool_name == "Bash"]) == 1 + assert {m.parent_tool_use_id for m in record.messages if getattr(m, "parent_tool_use_id", None)} == {"call_1"} - def test_forked_parent_history_is_not_credited_to_the_child(self, tmp_path): - # fork_turns copies the parent's history ahead of the child's own; the - # parent's shell call in that prefix must not be replayed as the child's. - child, parent = "019e0000-1111-7000-8000-000000000008", "019e0000-0000-7000-8000-000000000000" - _write_child_rollout( + async def test_a_failed_or_cut_off_child_call_is_not_recovered_as_a_success(self, monkeypatch, tmp_path): + # A require_success criterion reads result_status: a failed or cut-off call must not reach it as a success. + monkeypatch.setenv("CODEX_HOME", str(tmp_path)) + child = "019e0000-cdcd-7000-8000-00000000000c" + write_child_rollout( tmp_path, child, [ - {"type": "session_meta", "payload": {"id": child, "forked_from_id": parent}}, - {"type": "session_meta", "payload": {"id": parent}}, { "type": "function_call", "name": "shell_command", - "call_id": "p_1", - "arguments": '{"command":"git status"}', + "call_id": "c_1", + "arguments": '{"command":"pytest"}', + }, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 1\nOutput:\n1 failed\n"}, + { + "type": "function_call", + "name": "exec_command", + "call_id": "c_2", + "arguments": '{"cmd":"make build"}', + }, + { + "type": "function_call_output", + "call_id": "c_2", + "output": "Chunk ID: ab12\nWall time: 0.4 seconds\nProcess exited with code 2\nOutput:\nboom\n", }, - {"type": "function_call_output", "call_id": "p_1", "output": "Exit code: 0\nOutput:\n"}, - {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, - {"type": "turn_context", "payload": {"turn_id": "t_child"}}, - {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, - {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\nREADME.md\n"}, + exec_call("c_3", "await tools.view_image({path: 'missing.png'});"), + exec_output("c_3", "Error: no such file", head="Script failed"), + { + "type": "function_call", + "name": "exec_command", + "call_id": "c_4", + "arguments": '{"cmd":"sleep 60"}', + }, + {"type": "function_call_output", "call_id": "c_4", "output": "Wall time: 3.2 seconds\naborted by user"}, ], ) - gens = CodexAgent._parse_rollout_generations(CodexAgent._find_rollout_file(tmp_path, child)) - assert [t["parameters"]["command"] for g in gens for t in g["tools"]] == ["ls"] + spawn = _collab_call("spawnAgent", call_id="call_spawn", child_thread=child) + notifications = [ + _item_notification("item/started", spawn), + _item_notification("item/completed", spawn), + _delta("done"), + _turn_completed(), + ] + agent = _started_agent(parse_agent_config(type=AgentKind.CODEX), notifications) - def test_rollout_without_fork_marker_is_parsed_whole(self, tmp_path): - child = "019e0000-2222-7000-8000-000000000009" - _write_child_rollout( - tmp_path, - child, - [ - {"type": "session_meta", "payload": {"id": child}}, - {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, - {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\n"}, - {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, - ], - ) - gens = CodexAgent._parse_rollout_generations(CodexAgent._find_rollout_file(tmp_path, child)) - assert [t["parameters"]["command"] for g in gens for t in g["tools"]] == ["ls"] + record = await agent.communicate("go") + + recovered = [c for c in record.commands if c.tool_id.startswith("sub:")] + assert [(c.tool_name, c.result_status) for c in recovered] == [ + ("Bash", "error"), + ("Bash", "error"), + ("exec", "error"), + ("Bash", "unknown"), + ] + blocks = [ + b for m in record.messages for b in m.content_blocks if b.tool_use_id in {c.tool_id for c in recovered} + ] + assert [b.is_error for b in blocks] == [True, True, True, False] async def test_missing_rollout_is_silently_skipped(self, monkeypatch, tmp_path): # No rollout written for the child → recovery finds nothing and the turn @@ -1569,7 +1437,7 @@ async def test_generations_carry_per_generation_tokens_in_order(self, monkeypatc # output tokens must be 90, not 0). monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-cccc-7000-8000-000000000003" - _write_child_rollout( + write_child_rollout( tmp_path, child, [ @@ -1581,10 +1449,10 @@ async def test_generations_carry_per_generation_tokens_in_order(self, monkeypatc }, {"type": "function_call_output", "call_id": "c_py", "output": "5050\n"}, # gen1 boundary: the tool-call generation (90 output tokens). - _token_count_event(inp=11860, cached=3456, out=90, tot_in=11860, tot_cached=3456, tot_out=90), + token_count_event(inp=11860, cached=3456, out=90, tot_in=11860, tot_cached=3456, tot_out=90), {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "5050"}]}, # gen2 boundary: the final reply (6 output tokens). Totals cumulative. - _token_count_event(inp=11999, cached=11648, out=6, tot_in=23859, tot_cached=15104, tot_out=96), + token_count_event(inp=11999, cached=11648, out=6, tot_in=23859, tot_cached=15104, tot_out=96), ], ) spawn = _collab_call("spawnAgent", call_id="call_spawn", model="gpt-5.5", child_thread=child) @@ -1628,13 +1496,13 @@ async def test_fold_does_not_double_count_parent_plus_child(self, monkeypatch, t monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-dddd-7000-8000-000000000004" # Child cumulative: cache_read 15104, fresh 8755 (23859-15104), output 96. - _write_child_rollout( + write_child_rollout( tmp_path, child, [ {"type": "function_call", "name": "exec_command", "call_id": "c_py", "arguments": '{"cmd":"x"}'}, {"type": "function_call_output", "call_id": "c_py", "output": "5050"}, - _token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), + token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), ], ) spawn = _collab_call("spawnAgent", call_id="call_spawn", model="gpt-5.5", child_thread=child) @@ -2427,7 +2295,8 @@ def should_stop() -> bool: assert record.max_turns_exhausted is False assert len(record.commands) == 1 - async def test_capped_turn_still_folds_sub_agent_tokens(self, monkeypatch, tmp_path): + @pytest.mark.parametrize("shape", ["v1", "v2"]) + async def test_capped_turn_still_folds_sub_agent_tokens(self, monkeypatch, tmp_path, shape): """A capped turn must not lose the child threads' spend. Codex bills sub-agents on separate threads the parent total never sees, and @@ -2439,16 +2308,16 @@ async def test_capped_turn_still_folds_sub_agent_tokens(self, monkeypatch, tmp_p """ monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-eeee-7000-8000-000000000005" - _write_child_rollout( + write_child_rollout( tmp_path, child, [ {"type": "function_call", "name": "exec_command", "call_id": "c_py", "arguments": '{"cmd":"x"}'}, {"type": "function_call_output", "call_id": "c_py", "output": "5050"}, - _token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), + token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), ], ) - spawn = _collab_call("spawnAgent", call_id="call_spawn", model="gpt-5.5", child_thread=child) + spawn = _spawn(shape, child) wait = _collab_call("wait", call_id="call_wait", result="5050", child_thread=child) # The cap fires once the second call's tools finish, before turn/completed is dispatched. notifications = [ @@ -2474,20 +2343,21 @@ async def test_capped_turn_still_folds_sub_agent_tokens(self, monkeypatch, tmp_p assert record.token_usage.output_tokens >= 96 assert record.token_usage.cache_read_input_tokens >= 15104 - async def test_cooperative_stop_still_skips_sub_agent_recovery(self, monkeypatch, tmp_path): + @pytest.mark.parametrize("shape", ["v1", "v2"]) + async def test_cooperative_stop_still_skips_sub_agent_recovery(self, monkeypatch, tmp_path, shape): """The early-stop path keeps its pre-existing skip: an armed gate already decided.""" monkeypatch.setenv("CODEX_HOME", str(tmp_path)) child = "019e0000-ffff-7000-8000-000000000006" - _write_child_rollout( + write_child_rollout( tmp_path, child, [ {"type": "function_call", "name": "exec_command", "call_id": "c_py", "arguments": '{"cmd":"x"}'}, {"type": "function_call_output", "call_id": "c_py", "output": "5050"}, - _token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), + token_count_event(inp=23859, cached=15104, out=96, tot_in=23859, tot_cached=15104, tot_out=96), ], ) - spawn = _collab_call("spawnAgent", call_id="call_spawn", model="gpt-5.5", child_thread=child) + spawn = _spawn(shape, child) wait = _collab_call("wait", call_id="call_wait", result="5050", child_thread=child) notifications = [ _item_notification("item/started", spawn), diff --git a/tests/test_codex_agent_unit.py b/tests/test_codex_agent_unit.py index 982eecfe..e03f83e9 100644 --- a/tests/test_codex_agent_unit.py +++ b/tests/test_codex_agent_unit.py @@ -14,6 +14,7 @@ from __future__ import annotations +import logging import os import time from datetime import datetime @@ -22,8 +23,21 @@ import pytest -from coder_eval.agents.codex_agent import _CLAUDE_TO_CODEX_TOOL_MAP, CodexAgent +from coder_eval.agents.codex_agent import _CLAUDE_TO_CODEX_TOOL_MAP, CodexAgent, _record_command_text from coder_eval.models import AgentKind, parse_agent_config +from tests._fixtures.codex_rollouts import ( + command_record, + exec_call, + exec_output, + file_change_record, + token_count_event, + wait_call, + wait_output, + write_child_rollout, +) + + +_BOUNDARY = token_count_event(inp=10, cached=0, out=5, tot_in=10, tot_cached=0, tot_out=5) def _item_notification(method: str, root: SimpleNamespace) -> SimpleNamespace: @@ -348,3 +362,318 @@ async def fake_turn(user_input, **_): assert await agent.communicate("go") == "record" assert not (agent.working_directory / ".agents").exists() assert seen == [True, True] + + +class TestSubAgentRolloutRecovery: + """A sub-agent's tool rows are rebuilt from its rollout file: pure parsing, no SDK. + + A code-mode ``exec`` script's rows are the ``item_completed`` records Codex writes + while it runs, each with its own result. + """ + + @staticmethod + def _rows(path: Path) -> list[tuple[str, str, dict]]: + gens = CodexAgent._parse_rollout_generations(path) + return [(c["tool_name"], c["status"], c["parameters"]) for g in gens for c in g["tools"]] + + def test_a_scripts_rows_are_its_records_each_with_its_own_result(self, tmp_path): + script = ( + "await tools.apply_patch(p);" + ' await Promise.all([tools.exec_command({cmd:"pytest -q"}), tools.exec_command({cmd:"ls nope"})]);' + ) + path = write_child_rollout( + tmp_path, + "t1", + [ + exec_call("c1", script), + file_change_record("/w/a.txt", item_id="exec-1"), + command_record("pytest -q", exit_code=0, item_id="exec-2"), + command_record("ls nope", exit_code=1, item_id="exec-3"), + exec_output("c1", '[{"exit_code":0},{"exit_code":1}]'), + ], + ) + + gens = CodexAgent._parse_rollout_generations(path) + + # `Script completed` says only that the script ended; each record keeps its own result. + assert [(c["call_id"], c["tool_name"], c["status"], c["parameters"]) for g in gens for c in g["tools"]] == [ + ("exec-1", "Write", "success", {"paths": ["/w/a.txt"]}), + ("exec-2", "Bash", "success", {"command": "pytest -q"}), + ("exec-3", "Bash", "error", {"command": "ls nope"}), + ] + + def test_a_script_that_ran_nothing_recorded_is_one_exec_row(self, tmp_path): + script = "text(ALL_TOOLS.filter(x => /agent/.test(x.name)));" + path = write_child_rollout(tmp_path, "t1", [exec_call("c1", script), exec_output("c1", "[]")]) + + assert self._rows(path) == [("exec", "success", {"input": script})] + + @pytest.mark.parametrize( + ("output", "status"), + [ + (exec_output("c1", "TypeError: x is undefined", head="Script failed"), "error"), + ({"type": "custom_tool_call_output", "call_id": "c1", "output": "code-mode host is disabled"}, "error"), + (None, "unknown"), + (exec_output("c1", head="Script running with cell ID 3"), "unknown"), + (exec_output("c1", head="Script terminated"), "unknown"), + ({"type": "custom_tool_call_output", "call_id": "c1", "output": "aborted by user after 2.5s"}, "unknown"), + ], + ids=["script-failed", "host-disabled", "no-output", "still-running", "terminated", "aborted"], + ) + def test_an_exec_row_status_comes_from_the_script_output(self, tmp_path, output, status): + lines = [exec_call("c1", "await tools.view_image({path:'a.png'});")] + if output is not None: + lines.append(output) + + assert self._rows(write_child_rollout(tmp_path, "t1", lines))[0][1] == status + + @pytest.mark.parametrize( + ("script", "recorded"), + [ + ( + 'const c = "rm -rf build"; await tools.exec_command({cmd: "ls"}); await tools.exec_command({cmd: c});', + ["ls", "rm -rf build"], + ), + ( + 'await tools.shell_command({justification:"re-run the command: tests", command:"pytest -q"});', + ["pytest -q"], + ), + ('for (const f of ["a", "b"]) await tools.exec_command({cmd: `cat ${f}`});', ["cat a", "cat b"]), + ], + ids=["variable", "key-inside-a-string", "loop"], + ) + def test_commands_come_from_the_records_not_the_script_text(self, tmp_path, script, recorded): + lines = [exec_call("c1", script)] + lines += [command_record(cmd, exit_code=0, item_id=f"exec-{i}") for i, cmd in enumerate(recorded)] + lines.append(exec_output("c1")) + + assert [p["command"] for _, _, p in self._rows(write_child_rollout(tmp_path, "t1", lines))] == recorded + + def test_a_record_of_a_direct_call_is_not_a_row(self, tmp_path): + path = write_child_rollout( + tmp_path, + "t1", + [ + exec_call("c1", "text(1);"), + {"type": "function_call", "name": "exec_command", "call_id": "c2", "arguments": '{"cmd":"ls"}'}, + command_record("ls", exit_code=0, item_id="c2"), + {"type": "function_call_output", "call_id": "c2", "output": "Exit code: 0\nOutput:\na\n"}, + exec_output("c1", "1"), + ], + ) + + assert self._rows(path) == [ + ("exec", "success", {"input": "text(1);"}), + ("Bash", "success", {"cmd": "ls", "command": "ls"}), + ] + + def test_a_late_record_matching_no_script_source_goes_to_the_newest_script(self, tmp_path): + # The command is built from a variable, so no script's source holds its text, + # and it ends after its script: it is still that script's row, not dropped. + script = "for (const f of files) await tools.exec_command({cmd:`pytest ${f}`});" + path = write_child_rollout( + tmp_path, + "t1", + [ + exec_call("c1", script), + exec_output("c1", '{"session_id":4}'), + command_record("pytest test_a.py", exit_code=0, item_id="exec-1"), + ], + ) + + assert self._rows(path) == [("Bash", "success", {"command": "pytest test_a.py"})] + + def test_a_record_in_a_rollout_with_no_script_is_not_a_row(self, tmp_path): + path = write_child_rollout(tmp_path, "t1", [command_record("ls", exit_code=0, item_id="exec-1")]) + + assert self._rows(path) == [] + + @pytest.mark.parametrize( + ("source_cmd", "command"), + [ + ("sh -c 'sleep 45; exit 5'", "sh -c 'sleep 45; exit 5'"), + ('pytest C:\\\\w\\\\tests -k \\"slow\\"', 'pytest C:\\w\\tests -k "slow"'), + ], + ids=["as-written", "escaped-in-the-source"], + ) + def test_a_long_command_polled_by_a_later_script_replaces_its_own_script(self, tmp_path, source_cmd, command): + # The shape of a real CLI 0.156.1 run: the command outlives its script, a second + # script polls it with write_stdin, and its record arrives while that one runs. + start = ( + f'const r = await tools.exec_command({{cmd:"{source_cmd}",yield_time_ms:30000}}); text(JSON.stringify(r));' + ) + poll = ( + 'const r = await tools.write_stdin({session_id:27738,chars:"",yield_time_ms:30000});' + " text(JSON.stringify(r));" + ) + path = write_child_rollout( + tmp_path, + "t1", + [ + exec_call("c1", start), + exec_output("c1", '{"session_id":27738}'), + _BOUNDARY, + exec_call("c2", poll), + command_record(command, exit_code=5, item_id="exec-1"), + exec_output("c2", '{"exit_code":5}'), + _BOUNDARY, + ], + ) + + gens = CodexAgent._parse_rollout_generations(path) + + # The command's row sits in the generation that started it, so no generation is left empty. + assert [[(c["tool_name"], c["status"], c["parameters"]) for c in g["tools"]] for g in gens] == [ + [("Bash", "error", {"command": command})], + [("exec", "success", {"input": poll})], + ] + + def test_a_record_that_arrives_during_a_wait_goes_to_the_yielded_script(self, tmp_path): + path = write_child_rollout( + tmp_path, + "t1", + [ + exec_call("c1", 'await tools.exec_command({cmd:"make build", yield_time_ms:120000});'), + exec_output("c1", head="Script running with cell ID 3"), + _BOUNDARY, + wait_call("w1", "3"), + wait_output("w1", head="Script running with cell ID 3"), + _BOUNDARY, + wait_call("w2", "3"), + command_record("make build", exit_code=2, item_id="exec-1"), + wait_output("w2", head="Script failed"), + _BOUNDARY, + ], + ) + + gens = CodexAgent._parse_rollout_generations(path) + + assert [[(c["tool_name"], c["status"]) for c in g["tools"]] for g in gens] == [ + [("Bash", "error")], + [("wait", "unknown")], + [("wait", "error")], + ] + + def test_a_yielded_script_without_records_stays_unknown_and_its_wait_is_a_row(self, tmp_path): + lines = [ + exec_call("c1", "await tools.view_image({path:'a.png'});"), + exec_output("c1", head="Script running with cell ID 9"), + wait_call("w1", "9"), + wait_output("w1", head="Script completed"), + ] + + assert [row[:2] for row in self._rows(write_child_rollout(tmp_path, "t1", lines))] == [ + ("exec", "unknown"), + ("wait", "success"), + ] + + @pytest.mark.parametrize( + ("output", "status"), + [ + ("Exit code: 0\nWall time: 1 seconds\nOutput:\nok\n", "success"), + ("Exit code: 1\nWall time: 1 seconds\nOutput:\nboom\n", "error"), + ("Chunk ID: ab12\nWall time: 0.2 seconds\nProcess exited with code 2\nOutput:\nboom\n", "error"), + ("Chunk ID: ab12\nWall time: 0.2 seconds\nProcess exited with code 0\nOutput:\nExit code: 1\n", "success"), + ("5050\n", "success"), + ( + "Chunk ID: ab12\nWall time: 10.0 seconds\nProcess running with session ID 7\nOutput:\nstill going\n", + "unknown", + ), + ("Wall time: 3.2 seconds\naborted by user", "unknown"), + ("aborted by user after 3.2s", "unknown"), + ("Exit code: 0\nWall time: 1 seconds\nOutput:\naborted by user\n", "success"), + ("write_stdin failed: Unknown process id 7", "error"), + ], + ids=[ + "v1-ok", + "v1-failed", + "unified-exec-failed", + "exit-line-in-the-body", + "no-header", + "unified-exec-still-running", + "shell-aborted", + "call-aborted", + "abort-text-in-the-body", + "call-failed", + ], + ) + def test_a_direct_shell_call_status_comes_from_its_output_header(self, output, status): + assert CodexAgent._subagent_output({"output": output}) == (output, status) + + @pytest.mark.parametrize( + ("argv", "text"), + [ + (["/bin/zsh", "-lc", "pytest -q"], "pytest -q"), + (["bash", "-c", "ls -la"], "ls -la"), + (["C:\\Program Files\\PowerShell\\7\\pwsh.exe", "-NoProfile", "-Command", "git status"], "git status"), + (["cmd.exe", "/d", "/c", "dir"], "dir"), + (["python3", "-c", "print(1)"], "python3 -c 'print(1)'"), + ("echo hi", "echo hi"), + ], + ids=["zsh-login", "bash", "powershell", "cmd", "not-a-shell", "string"], + ) + def test_record_command_text_drops_only_the_shell_wrapper(self, argv, text): + assert _record_command_text(argv) == text + + def test_a_line_that_is_not_a_json_object_is_skipped(self, tmp_path): + path = write_child_rollout(tmp_path, "t1", [exec_call("c1", "text(1);"), exec_output("c1", "1")]) + path.write_text('"a string"\n[1, 2]\n' + path.read_text(encoding="utf-8"), encoding="utf-8") + + assert self._rows(path) == [("exec", "success", {"input": "text(1);"})] + + def test_forked_parent_history_is_not_credited_to_the_child(self, tmp_path): + # fork_turns copies the parent's history ahead of the child's own; the + # parent's shell call in that prefix must not be replayed as the child's. + child, parent = "019e0000-1111-7000-8000-000000000008", "019e0000-0000-7000-8000-000000000000" + path = write_child_rollout( + tmp_path, + child, + [ + {"type": "session_meta", "payload": {"id": child, "forked_from_id": parent}}, + {"type": "session_meta", "payload": {"id": parent}}, + { + "type": "function_call", + "name": "shell_command", + "call_id": "p_1", + "arguments": '{"command":"git status"}', + }, + {"type": "function_call_output", "call_id": "p_1", "output": "Exit code: 0\nOutput:\n"}, + {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, + {"type": "turn_context", "payload": {"turn_id": "t_child"}}, + {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\nREADME.md\n"}, + ], + ) + + assert [p["command"] for _, _, p in self._rows(path)] == ["ls"] + + def test_rollout_without_fork_marker_is_parsed_whole(self, tmp_path): + child = "019e0000-2222-7000-8000-000000000009" + path = write_child_rollout( + tmp_path, + child, + [ + {"type": "session_meta", "payload": {"id": child}}, + {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\n"}, + {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": child}}, + ], + ) + + assert [p["command"] for _, _, p in self._rows(path)] == ["ls"] + + def test_a_fork_without_its_own_settings_marker_is_parsed_whole_and_logged(self, tmp_path, caplog): + child, parent = "019e0000-3333-7000-8000-00000000000a", "019e0000-0000-7000-8000-000000000000" + path = write_child_rollout( + tmp_path, + child, + [ + {"type": "session_meta", "payload": {"id": child, "forked_from_id": parent}}, + {"type": "function_call", "name": "shell_command", "call_id": "c_1", "arguments": '{"command":"ls"}'}, + {"type": "function_call_output", "call_id": "c_1", "output": "Exit code: 0\nOutput:\n"}, + ], + ) + + with caplog.at_level(logging.WARNING, logger="coder_eval.agents.codex_agent"): + assert [p["command"] for _, _, p in self._rows(path)] == ["ls"] + assert child in caplog.text From a6e5f2c26688df0d23c523d3416a37791090c185 Mon Sep 17 00:00:00 2001 From: Bai Li Date: Mon, 19 Oct 2026 11:49:43 -0700 Subject: [PATCH 3/4] fix(codex): match a multi-line command record to its exec script A script source writes a multi-line command as a `"a\nb"` literal, while its CommandExecution record holds real newlines, so the backslash-stripped match missed it. When such a command outlived its script, its record went to a later script, and the script that ran it was left as an extra `exec` row. - Decode the source's JavaScript string escapes before the match, once per script rather than once per record. - Add the real failing shape to the polled-command test, and a decoder test. - Notes and CODEX.md: headless threads through 0.147 write no records; the 0.147 TUI and 0.155.1 do. Co-Authored-By: Claude Opus 5.5 --- .claude/notes/agents.md | 16 ++++++----- docs/agents/CODEX.md | 4 +-- src/coder_eval/agents/codex_agent.py | 40 ++++++++++++++++++++-------- tests/test_codex_agent_unit.py | 27 +++++++++++++++++-- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/.claude/notes/agents.md b/.claude/notes/agents.md index 6bb0bfef..e11d1268 100644 --- a/.claude/notes/agents.md +++ b/.claude/notes/agents.md @@ -424,17 +424,21 @@ command. A command can outlive its script: `tools.exec_command` returns at the c `yield_time_ms` with a session id, the script ends, and a later script polls the command with `tools.write_stdin`. The text match then gives the record to the script that started the command, so that script is not also left as an `exec` row that repeats the command. -The match ignores backslashes, because the source escapes the backslashes and double -quotes that the command holds; without that, no record of a real 0.156.1 Windows e2e -child matched. A record that matches no source goes to the newest script: dropping it +The match decodes the source's string escapes and then ignores backslashes. A multi-line +command is a `"a\nb"` literal in the source but holds real newlines in its record; in a +recorded 0.147 TUI thread, 3 of 10 such commands outlived their script and, undecoded, +went to a later one. The source also escapes the backslashes and double quotes that the +command holds; without ignoring backslashes, no record of a real 0.156.1 Windows e2e child +matched. A record that matches no source goes to the newest script: dropping it would lose a command built from a variable, the miss this recovery exists to prevent, while a wrong guess only moves the row to another generation. A record whose id is a direct call's is dropped, so a direct call is never counted twice. Seen on real CLI 0.156.1 runs, v1 and v2 children alike: the records sit between the script call and its output, before the generation's `token_count`, and a 45-second command was polled as -described. CLIs through 0.147 write no `CommandExecution` records, so there a code-mode -script stays one `exec` row and its commands are not recoverable; 0.156.1 and 0.162 write -them, on Windows too; 0.155, the SDK floor, is not checked. +described. Headless threads through 0.147 (the 0.144 SDK, 0.147 `codex exec`) write no +`CommandExecution` records, so there a code-mode script stays one `exec` row and its +commands are not recoverable. The 0.147 TUI and 0.155.1 (the SDK floor) write them, as do +0.156.1 and 0.162, on Windows too. A call with no result is `unknown`, never a success, so `require_success` does not credit it: an interrupt cut it off, it still ran after its yield time, or `wait` stopped it. The diff --git a/docs/agents/CODEX.md b/docs/agents/CODEX.md index 87e23674..f434c3ce 100644 --- a/docs/agents/CODEX.md +++ b/docs/agents/CODEX.md @@ -228,8 +228,8 @@ spawning `Agent` row. Spawns from multi-agent v1 (`spawnAgent`) and v2 - Each file the child patched is a `Write` row. - A code-mode `exec` script that ran no command and changed no file (for example, one that only reads the tool list) is one `exec` row, not a `Bash` row. -- CLI 0.147 and earlier write no command records; 0.156.1 and 0.162 do, and the versions - between them are not checked. On a CLI without records a code-mode script is one `exec` +- Headless Codex through 0.147 writes no command records; 0.155.1 (the SDK floor), + 0.156.1 and 0.162 do. On a CLI without records a code-mode script is one `exec` row, and the commands inside it are not recovered. - A multi-agent v2 spawn names no model, so the child's generations are priced at the parent's model. diff --git a/src/coder_eval/agents/codex_agent.py b/src/coder_eval/agents/codex_agent.py index 4b201081..86fc8171 100644 --- a/src/coder_eval/agents/codex_agent.py +++ b/src/coder_eval/agents/codex_agent.py @@ -180,6 +180,9 @@ # flag that precedes the command (a `-c` cluster such as `-lc`, `-Command`, `/c`). _SHELL_PROGRAM = re.compile(r"(?:.*[\\/])?(?:[a-z]*sh|powershell|cmd)(?:\.exe)?", re.IGNORECASE) _SHELL_SCRIPT_FLAG = re.compile(r"-[a-z]*c|-command|/c", re.IGNORECASE) +# A JavaScript string escape: `\n`, `\u00e9`, `\u{1F600}`, `\x41`, or any escaped character. +_JS_ESCAPE = re.compile(r"\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)", re.DOTALL) +_JS_CHAR_ESCAPES = {"n": "\n", "t": "\t", "r": "\r", "b": "\b", "f": "\f", "v": "\v", "0": "\0", "\n": ""} _ResultStatus = Literal["success", "error", "unknown"] @@ -272,6 +275,21 @@ def _record_command_text(argv: Any) -> str: return shlex.join(parts) +def _js_unescaped(source: str) -> str: + """A script source with its string escapes decoded: a ``"a\\nb"`` literal reads as the two-line command it runs.""" + + def decode(match: re.Match[str]) -> str: + escape = match[1] + if len(escape) == 1: + return _JS_CHAR_ESCAPES.get(escape, escape) + try: + return chr(int(escape.strip("ux{}"), 16)) + except ValueError: + return match[0] + + return _JS_ESCAPE.sub(decode, source) + + def _file_change_outcome(paths: list[str], status: str) -> tuple[_ResultStatus, str]: """(result_status, result_summary) of a Codex file change; a failed or declined patch wrote nothing.""" if status in _FILE_CHANGE_FAILURE_STATUSES: @@ -2127,9 +2145,9 @@ def _parse_rollout_generations(cls, path: Path) -> list[dict[str, Any]]: cur: list[dict[str, Any]] = [] n_calls = 0 direct_call_ids: set[str] = set() - # Every exec script, and the records each one wrote, by its call id: a - # script's records replace its row. - scripts: list[dict[str, Any]] = [] + # Every exec script with the source text a record is matched against, and the + # records each one wrote, by its call id: a script's records replace its row. + scripts: list[tuple[dict[str, Any], str]] = [] records: dict[str, list[dict[str, Any]]] = {} def close(tokens: tuple[int, int, int, int] | None) -> None: @@ -2159,7 +2177,8 @@ def close(tokens: tuple[int, int, int, int] | None) -> None: "status": status, } if name == _EXEC_TOOL: - scripts.append(call) + source = _js_unescaped(str(call["parameters"].get("input", ""))).replace("\\", "") + scripts.append((call, source)) else: direct_call_ids.add(call_id) cur.append({"kind": "tool", "call": call}) @@ -2347,20 +2366,19 @@ def _record_call(item: Any, fallback_id: str) -> dict[str, Any] | None: return None @staticmethod - def _record_owner(record: dict[str, Any], scripts: list[dict[str, Any]]) -> dict[str, Any] | None: + def _record_owner(record: dict[str, Any], scripts: list[tuple[dict[str, Any], str]]) -> dict[str, Any] | None: """The exec script a record came from. - The newest script whose source holds the record's command, compared with - backslashes removed; else the newest script. None only when the rollout holds - no script. + The newest script whose source holds the record's command, compared with the + source's string escapes decoded and backslashes removed; else the newest + script. None only when the rollout holds no script. Rationale: .claude/notes/agents.md § Codex rollout rebuild """ command = record["parameters"].get("command") text = str(command or "").replace("\\", "") - sources = ((s, str(s["parameters"].get("input", "")).replace("\\", "")) for s in reversed(scripts)) - owner = next((s for s, source in sources if text and text in source), None) + owner = next((s for s, source in reversed(scripts) if text and text in source), None) if owner is None and scripts: - owner = scripts[-1] + owner = scripts[-1][0] logger.debug( "CodexAgent: %s record %r matches no script source; attributed to script %s", record["tool_name"], diff --git a/tests/test_codex_agent_unit.py b/tests/test_codex_agent_unit.py index e03f83e9..4b51a2e8 100644 --- a/tests/test_codex_agent_unit.py +++ b/tests/test_codex_agent_unit.py @@ -23,7 +23,12 @@ import pytest -from coder_eval.agents.codex_agent import _CLAUDE_TO_CODEX_TOOL_MAP, CodexAgent, _record_command_text +from coder_eval.agents.codex_agent import ( + _CLAUDE_TO_CODEX_TOOL_MAP, + CodexAgent, + _js_unescaped, + _record_command_text, +) from coder_eval.models import AgentKind, parse_agent_config from tests._fixtures.codex_rollouts import ( command_record, @@ -493,8 +498,12 @@ def test_a_record_in_a_rollout_with_no_script_is_not_a_row(self, tmp_path): [ ("sh -c 'sleep 45; exit 5'", "sh -c 'sleep 45; exit 5'"), ('pytest C:\\\\w\\\\tests -k \\"slow\\"', 'pytest C:\\w\\tests -k "slow"'), + ( + "set +e\\nprintf '%s\\\\n' '== uip =='\\nuip --version", + "set +e\nprintf '%s\\n' '== uip =='\nuip --version", + ), ], - ids=["as-written", "escaped-in-the-source"], + ids=["as-written", "escaped-in-the-source", "multi-line"], ) def test_a_long_command_polled_by_a_later_script_replaces_its_own_script(self, tmp_path, source_cmd, command): # The shape of a real CLI 0.156.1 run: the command outlives its script, a second @@ -615,6 +624,20 @@ def test_a_direct_shell_call_status_comes_from_its_output_header(self, output, s def test_record_command_text_drops_only_the_shell_wrapper(self, argv, text): assert _record_command_text(argv) == text + @pytest.mark.parametrize( + ("source", "decoded"), + [ + ("a\\nb\\tc", "a\nb\tc"), + ("printf '%s\\\\n' \\\"x\\\"", "printf '%s\\n' \"x\""), + ("caf\\u00e9 \\u{1F600} \\x41", "caf\u00e9 \U0001f600 A"), + ("one \\\ntwo", "one two"), + ("\\u{110000}", "\\u{110000}"), + ], + ids=["newline-tab", "escaped-backslash-and-quote", "unicode-and-hex", "line-continuation", "bad-code-point"], + ) + def test_js_unescaped_decodes_string_escapes(self, source, decoded): + assert _js_unescaped(source) == decoded + def test_a_line_that_is_not_a_json_object_is_skipped(self, tmp_path): path = write_child_rollout(tmp_path, "t1", [exec_call("c1", "text(1);"), exec_output("c1", "1")]) path.write_text('"a string"\n[1, 2]\n' + path.read_text(encoding="utf-8"), encoding="utf-8") From 319d0ae71cf7806b4f76247693be4e4c04239d57 Mon Sep 17 00:00:00 2001 From: Bai Li Date: Mon, 19 Oct 2026 11:57:37 -0700 Subject: [PATCH 4/4] fix(deps): bump aiohttp 3.14.3 -> 3.14.4 for CVE-2026-111063 and CVE-2026-111064 pip-audit in the Quality Gate flags aiohttp 3.14.3 for CVE-2026-111063, CVE-2026-111064 and GHSA-2g87-pp6c-29x5, all fixed in 3.14.4. aiohttp is a transitive dependency, so only uv.lock changes; yarl moves to 1.25.1 with it. Co-Authored-By: Claude Opus 5.5 --- uv.lock | 343 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 199 insertions(+), 144 deletions(-) diff --git a/uv.lock b/uv.lock index cae203b8..206f939d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -42,7 +42,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.3" +version = "3.14.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -53,72 +53,114 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, - { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, - { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, - { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, - { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, - { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, - { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, - { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, - { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, - { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, - { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, - { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, - { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, - { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, - { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, - { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, - { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, - { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, - { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, - { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, - { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, - { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, - { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, - { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, - { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, - { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, - { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/93/2f/6a91adaa2dc26877d6ed2f54c0370c8910f019db7d77c5c6a194611e93ea/aiohttp-3.14.4.tar.gz", hash = "sha256:831fc5bd39ec2517851e348f613ddb5447a47cf4b71cb09845af7ad7ed45d8f9", size = 8077839, upload-time = "2026-10-05T17:43:48.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ec/7fee140da700ec3f5aa5e0fd2319f67be5e86de197e2384d27e5e6209e0e/aiohttp-3.14.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:264b5a7568306590c44ae4a0237ad1715e35d447ea44ebf324e4c8fd9f78f67a", size = 539114, upload-time = "2026-10-05T17:39:26.322Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/930c01ce9e186e787046433d7dc5b45b188885f8406c14b58435b4431974/aiohttp-3.14.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:17e5d7c7775d8dd8e894c7ef6ebfa26509de36868a9a6c7c4fcfaf6a1bf43d60", size = 553514, upload-time = "2026-10-05T17:39:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d7/983e3cea81c07e95f35d34da4d1e182b5c05489c49b7560b198a83f0da79/aiohttp-3.14.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e91dc8fe9cbd052d16f7f1267e285cf8bbac4d2194bd5741f05ef201c77ed09e", size = 521381, upload-time = "2026-10-05T17:39:30.419Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/80baed11659093883f51245755105557d3253e45bd0b693d70df0059d68b/aiohttp-3.14.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8df6157d31703972aec9e3b93963f039ff06318301e30b007c3e941ff8a7ac42", size = 528347, upload-time = "2026-10-05T17:39:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/c8/18/bfda255c311096c08dab26dc65f3d219f090df37dc5f5f6345f8c3aefc1f/aiohttp-3.14.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:220912b351549dc736c59d104dc443fa615753102bb778a4e98f86e4d94bff6a", size = 539914, upload-time = "2026-10-05T17:39:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/51/fc/3b6ca6117c0c42481d719411fff7b5d3970ebd906543d3dc1ba40fe2462f/aiohttp-3.14.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2efa42b39bb3f524d5eca3760639b584afdd1143a6b047fda29910ffb75e4993", size = 813997, upload-time = "2026-10-05T17:39:36.061Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f6/86e3e02a9712aadc1ae0a1076bb2d1adf3d9addfe5542a28190c8f9b6d9b/aiohttp-3.14.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e7356059c8262d2c1fd95242e2b7b70fbc6eee3135bf470049cbec3e968b5b0", size = 548058, upload-time = "2026-10-05T17:39:38.109Z" }, + { url = "https://files.pythonhosted.org/packages/85/7e/6b9c2e271419d40ca76589613b3c7ab185fd3050a407bd2565b6aa46f695/aiohttp-3.14.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fac247d0cc956d732df1211ac2d8a9999ec8dc1a92a90401de9d52e4ead546f7", size = 545933, upload-time = "2026-10-05T17:39:40.193Z" }, + { url = "https://files.pythonhosted.org/packages/14/fc/33d6e24a5f8ac32423230088c415e2a1c6efee23d902b62f20506631e40b/aiohttp-3.14.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2eb0f8b03b4f2bd154ed8cf7c1a119a1ad04808c258bf58f90138c83b8c6d5", size = 1888265, upload-time = "2026-10-05T17:39:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e8/2eca1d468d83a514621d1280f9992d08649191db2028c9236b4edfa7c723/aiohttp-3.14.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbe827d27e0e369fd7bb88ceeaa5b46ea006e73e015b9dd77e3723a53162293", size = 1872300, upload-time = "2026-10-05T17:39:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/5872983de71b0214acdfb86822f5cca295b39be4832ec589fa224aee0f6c/aiohttp-3.14.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70387146d03af9f047c629b71aa27d9594b3b31ec4c9c91e1a73d0d8bf38e0e", size = 1926213, upload-time = "2026-10-05T17:39:46.199Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/ebfefa11a546c33c717af63c67868408ecf0b33b23a9d75669af95485407/aiohttp-3.14.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d0934926fc65744e2fdd44ce68b2d79d5ee608a1e23f0596b35dd696519bb7c", size = 2056609, upload-time = "2026-10-05T17:39:48.221Z" }, + { url = "https://files.pythonhosted.org/packages/69/c0/70667e82ec041f4122a17afce711bbcf94a71bec5888b69a49aed8817be2/aiohttp-3.14.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff4b366510c733974adb7a4102c094e4a6308e6f369315a4cf14d2d0a619cf82", size = 1914740, upload-time = "2026-10-05T17:39:50.304Z" }, + { url = "https://files.pythonhosted.org/packages/bf/86/559e9dfea676206b20737d7ccb5b3716bff5e58d2a8d53682e5301724cd1/aiohttp-3.14.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:85fac7c99e0ac3dcbd6ae1c33774e0603bfc732ddb967a6fce4a731103c1888e", size = 1703390, upload-time = "2026-10-05T17:39:52.385Z" }, + { url = "https://files.pythonhosted.org/packages/fc/91/c98897d4cf23e120b2f3980042835cb34c86bbe3c653f3eaacf4bf986349/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef1e9c3b4a2300023a5dc865889ce532cfaf6798ba4ef78fbbefe738a9d23efa", size = 1852642, upload-time = "2026-10-05T17:39:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/14/13/0258f352cb2c236f358a5b57d138e15fa2566e17accc0758d968dbf2ceb6/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:97e65916588be0f952b3307aaa8460c0771fa98dbea2dd27dabc9f5d84d53c3b", size = 1890616, upload-time = "2026-10-05T17:39:56.466Z" }, + { url = "https://files.pythonhosted.org/packages/64/3c/f43e294ce1a06d1234a97bd862c1aa31acfa1a0eea3786c22fd0dc1ea731/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:840775cb39a9424f9edf142133a1192263ac5bd79117ad2a16602cef6a0c1e9d", size = 1896638, upload-time = "2026-10-05T17:39:58.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f7/ce1e3a85b73d05037d729d3d32da7d6959b9e433435e9abd46c2fb38410f/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:54f9256430ca040d58affbd551513de13a5cff7d065204fc52b9a1b0af09a049", size = 1693003, upload-time = "2026-10-05T17:40:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/45eb900d6a9ee046728f5589f3d07f5d78f049b5fb84f48881fa4174d5df/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6112d5931bf12c776188cab81b82bbb17d86e2e1381f2ade6edc43e6b240f0cb", size = 1927730, upload-time = "2026-10-05T17:40:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/b2/43/a83ff79dc8375953a1f00b808a21c82590f4e68304fecccdcc42a25fb7d8/aiohttp-3.14.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:facc6df39047c4732cbd885bb62e40df20885fcd709fc399e11f81b45ce0fff9", size = 1891177, upload-time = "2026-10-05T17:40:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/fbdc41608d4e2fb6084d960a41b392fa82beb5b126c2466de549b5c67fc8/aiohttp-3.14.4-cp313-cp313-win32.whl", hash = "sha256:7968634fa3a967a2bc0b9aeba1d5a1955501dcd7d0de44b96f7ebfb662797735", size = 479943, upload-time = "2026-10-05T17:40:07.402Z" }, + { url = "https://files.pythonhosted.org/packages/35/51/d75629750e705832fd5cd52516a82943ed58b6e09c30e14830f5d87452fa/aiohttp-3.14.4-cp313-cp313-win_amd64.whl", hash = "sha256:c0a894b0265d139cd4c2c8bd4643822cc289a4a5331e21e810881bd000ffb2dc", size = 506209, upload-time = "2026-10-05T17:40:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/02/64/66452d2ffcc13e5e5ee4ba4a968656fa4a98c29ed5ff0205d4fde02b2943/aiohttp-3.14.4-cp313-cp313-win_arm64.whl", hash = "sha256:da16c3037178e72589d91de59536d271dffc92c2be2b47c38a45538cba54fd29", size = 487163, upload-time = "2026-10-05T17:40:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/b653b1f969e2e949fe4726af7a8043e8ef117f202bed7757ac8564e077a1/aiohttp-3.14.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:749bfc43bb1df71bbe6ed215044d5412d786698fc5771d2037436ca49622f3c9", size = 540738, upload-time = "2026-10-05T17:40:14.109Z" }, + { url = "https://files.pythonhosted.org/packages/90/b5/eea49a0bb697deffac5165d707ec53a24748d6720b3af77a544107b49573/aiohttp-3.14.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:c113e4b489d711f606ca278cd8dc89179746244ea841aa0638b1b134600afad0", size = 554494, upload-time = "2026-10-05T17:40:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/11/ea/cf8f4efbbfa8572d9965cd43b3c6c84c581aa006772e5ce3ab4020027e9b/aiohttp-3.14.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:50969522bc7022026a2c7968e67da5c789f6b716b988edac55dc630390efd5df", size = 522653, upload-time = "2026-10-05T17:40:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/a3/24/9f6721f0b52323789cb1041ffc3afa729462d9925607e733a065b6fc5e17/aiohttp-3.14.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:cbf92cd7a1c800cee244558dd5630abb45c1c1093ef7b0d546a99f526ff7a9eb", size = 529697, upload-time = "2026-10-05T17:40:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a4/399debc3c2ca0ab88a08e3082b4c4f62d0f66978af60243c63f2345ef337/aiohttp-3.14.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fc76be08ff407fba717a769aff04ca1b4bf9ecfdcb5caadef84a563fdbbd146d", size = 540526, upload-time = "2026-10-05T17:40:23.548Z" }, + { url = "https://files.pythonhosted.org/packages/a2/97/90379813b18b795bccdeb3a5a85e9b93ac2c69f9c2a5d0faba59da3ad6d1/aiohttp-3.14.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9b416f995b68a921069e6ec61fa07f2ac3ff033d92e97a69f673af89f5bb954d", size = 818866, upload-time = "2026-10-05T17:40:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/78/70/824fd0dac819652f1df895b3f621a75bf0a3050c30e38d8d2316e34ab821/aiohttp-3.14.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2c9585101cfca10e74a07cc0ef2bdc6d90616e6750926b20456f429d0608f740", size = 549666, upload-time = "2026-10-05T17:40:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/eafb2dd200dd2b770ed25a2dc5731c4ba5d58ab3fa8e2e82494bb946808e/aiohttp-3.14.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3c4dcf611b095e7983421f49d01ffbed7ed574891533d606ea115fc23e7c4e90", size = 548961, upload-time = "2026-10-05T17:40:30.613Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/6c733a1c71300293471ac7ef1dffd12b5b4dc9a43c53461224b2b8a7f667/aiohttp-3.14.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8f61a9b5fa56c58e3441f9dc64137f786232d40fb89127190d0e0f84e1e5865", size = 1883165, upload-time = "2026-10-05T17:40:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/34/885ddbd351679a31141b25e0d1a5e4f67738806e564c858b399709b61239/aiohttp-3.14.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d43e538dfc85b705fd832620d050970ef5613b3869a7169b44a554b9dab9eee9", size = 1844119, upload-time = "2026-10-05T17:40:35.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ef/0a64dac53c11d569851efb3e7eddb43551df72b2c172ebb34cccc93bcd3f/aiohttp-3.14.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4135c4bb4b7682a7633fadc7170ea969d843f83aa7224e70111a20675105c985", size = 1930222, upload-time = "2026-10-05T17:40:37.527Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/774792ed8f973e0d234fcb06b0f0bc9f29e9a0a0dd847f6418c2904be7d9/aiohttp-3.14.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9055324f156a94fdf6dc9a825ae60f9a2947b88f591ea81ee80826633de62ae7", size = 2038842, upload-time = "2026-10-05T17:40:39.815Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/e00f35581c97656d9e864edaa03292382c620dd1d9c836a4f73a2279acc4/aiohttp-3.14.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb22c8e4b60385a9b414c70c6e8b2d0f4b23ff7fc4bb572ceb58297b228169f1", size = 1887845, upload-time = "2026-10-05T17:40:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2e/07e3eff5b4c1b082a2eaf0ffb508b409b21ea1ddc9fdc57f3eabb51469a1/aiohttp-3.14.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b6945559a260112742ed58cde2165488d2fc51aeea34afbaa99d9371b43e1b5", size = 1702533, upload-time = "2026-10-05T17:40:44.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/44/816844b7f876e6f8fee4ecb088269fcd92241da712e4814a4d624eda13a9/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f4078a90b174f15efb2c8725e8720c40dbd3b2cf6350dbc085c964a938d9de7", size = 1849058, upload-time = "2026-10-05T17:40:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bd/163242e12464a1f3e58a77e28beca949652ddce84abdd5500a91c70a1e0d/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f433c5654e32f72de3ef074d715eb5c7604eb6d28a5c2f3c9e5056f20fe6b306", size = 1854600, upload-time = "2026-10-05T17:40:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/59/5c1ae708a89524eddfd9c727b87cae26b5c50c3c76683b910ac06151ead0/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:871035fe79c636b6bc126ed78df37d72b830dfc751612cb4b1040360edf2719f", size = 1896565, upload-time = "2026-10-05T17:40:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/37/45/95300a7de69241fe47d3f838f3fb582c712782e172d447ff650a013ea673/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2617132524f66338f6c1d218cd16081576aefc40df4f2bcba338a99fbf67fec1", size = 1693092, upload-time = "2026-10-05T17:40:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/ff/33/01a8e61d48d0094121092c74125df14c0f8f10ee3149bfe68b6f869409a9/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d8bc326133272deee8ef337581550450e4bb735c0520be893b950a1f5baaf41a", size = 1917073, upload-time = "2026-10-05T17:40:57.656Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d6/1a4f24624cda6686d5b927e26d5d1ae3ebb692faf88e93017fab4b391fd0/aiohttp-3.14.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ec57223e261b7fa9979c12dbce009ca53439be76cac3af27f860d3e4b1d6f7a", size = 1872656, upload-time = "2026-10-05T17:41:00.123Z" }, + { url = "https://files.pythonhosted.org/packages/be/be/ed286fcfa2d22061ae021b568c790123505cd482df35e23023339ecaed96/aiohttp-3.14.4-cp314-cp314-win32.whl", hash = "sha256:7242043e71fc449a19a47d92f4e9fd1e0ae8d3f88488982381f422bf6d651218", size = 485461, upload-time = "2026-10-05T17:41:02.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/74/e040e6f65f4da305777f1adb3023d47401693489e4a337c983b52d2a0a6b/aiohttp-3.14.4-cp314-cp314-win_amd64.whl", hash = "sha256:019587c7b3a44917dd61bcb312fd5c034f341dc9851b83e7b41b65e08b68ca0f", size = 512147, upload-time = "2026-10-05T17:41:04.98Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5e/bddb33ae15fdd5a68a10a986587aa9916c418a712fdd110d3b3ae9b51407/aiohttp-3.14.4-cp314-cp314-win_arm64.whl", hash = "sha256:60dc71d38db0988f37e78f6ad4a010a2f6355cfac3fff5f130cf5ef2231f3f0d", size = 494685, upload-time = "2026-10-05T17:41:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/22/2e/7b884014b40573f919fc3a528e5028bc37b249e31a93928ba61bdd25c960/aiohttp-3.14.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:674f53ab9d2794e2dd767619a3384bf1006f8d075e43c19d449a645f8ff641c2", size = 855696, upload-time = "2026-10-05T17:41:09.514Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e6/b6e2b4f580ab6ae39241bb9182aca60e6e1915cdad0148faaa04db3f366b/aiohttp-3.14.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6d243221f73e5ac546c2ed9255100423e4bc092b08b944f23498e8cf3096c26c", size = 567931, upload-time = "2026-10-05T17:41:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/63/99/ae8e3a1a18adcbf636839d18b3cccc51007836ccdae5209381c5cf82d1ae/aiohttp-3.14.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0daa016eb74e7888ed5f9ceec6eea3158999c1c4a115ef5cbe88baa8d924f94c", size = 567421, upload-time = "2026-10-05T17:41:13.836Z" }, + { url = "https://files.pythonhosted.org/packages/63/38/c5d4bb58be72b967a4090d2deeb38571f8c992852936c51dbbbeae49db1c/aiohttp-3.14.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70c031a622aa20add05ebd234b90adaf6bf51c1c88cc305ec760721af83308c3", size = 2054173, upload-time = "2026-10-05T17:41:16.098Z" }, + { url = "https://files.pythonhosted.org/packages/eb/87/3c8a9c7670f56d478a75c5bb84b336fb61a67b908e36c30a4a96ed8c8e86/aiohttp-3.14.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7df363fa7c38945a896e12c69af4a1a56cbd4152791d9e41fd0bac396b72de38", size = 1918296, upload-time = "2026-10-05T17:41:18.478Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/bd00e499c0a1505b44f5e23b9739034ea9be792ad42537f2894211ba1c07/aiohttp-3.14.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02a508ba5ad91c4d730523584c0adc65882274bba5e37c33bc2f475e37577256", size = 2027513, upload-time = "2026-10-05T17:41:20.874Z" }, + { url = "https://files.pythonhosted.org/packages/23/1d/a3666a7c224f86a881f18d673a8ffdc92fa4c35274fb4361822f8f0dc566/aiohttp-3.14.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0cb3edece6fe6a944eaf97d803b88f606da6bc1600b1894c2969e78ffdc1fc29", size = 2152686, upload-time = "2026-10-05T17:41:23.181Z" }, + { url = "https://files.pythonhosted.org/packages/46/7e/f4204b8c4959c46f6367c182afbbacef08a1b53874281912c52fbd80151c/aiohttp-3.14.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21ce1223b461bcc5fb389a7204001578f1c0b85c2da60a84d915ebb021294245", size = 2006431, upload-time = "2026-10-05T17:41:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/33/43/091e69d42967cbea833ceb355c5457f8f70748fe08b88c9d91491b3fafb8/aiohttp-3.14.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35d5432f0dc1c8b71307e9112461b52f336da225e344283eec4779222c0a1dff", size = 1789101, upload-time = "2026-10-05T17:41:28.095Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/b57cbe533c01098c5d30471f057e7c3c19f281122f756acc9c716b636e2c/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6ec364493057c118d08292f7cd60865965c209167d8657b45643a26aef77495", size = 1974472, upload-time = "2026-10-05T17:41:30.499Z" }, + { url = "https://files.pythonhosted.org/packages/9f/44/79acce03611450e4ed8594f96f1cea40d70bc84fb4e918f94d7d9454a4af/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:70492983b7aed9e61bda82fe1ce80bcba9199610886443133783a288638923c0", size = 1956340, upload-time = "2026-10-05T17:41:33.009Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/36e6cbd6720e3377a02f8709aec6fe80de729ef41e78ba01f13d50363c73/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a77776673331577147e9d25cd7833a72e8891b47556336cff06ba1569e488b99", size = 1965771, upload-time = "2026-10-05T17:41:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/69/0f/879735ac70ce5e6852916eed51c706a517b205150a8d384f9fa60933a3fc/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:73799000dd6fd5247094fd8e6d3e1eb871e41aab5cc35dff9d9f654cd230bc24", size = 1780297, upload-time = "2026-10-05T17:41:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/7c030ca5b6baf3b02eccd11afc3d58f0b35e407031d5cc78cae9c24921c0/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:27b370fb532e707e8c96d04fa0c96301fb7dba2d50aaca57f6c456cdfffec5bd", size = 2001374, upload-time = "2026-10-05T17:41:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/ddacbb8141d727034c51dd192d17749521d5b30b03f84aa034325396089e/aiohttp-3.14.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:884e0d1c707b6217fee3080b9b4029eee7551af788ee761010df19be158414b7", size = 1968531, upload-time = "2026-10-05T17:41:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/3a/08/2214f823701eebc3e57db141faeae76df7bfd415f254cc7c1692258f228d/aiohttp-3.14.4-cp314-cp314t-win32.whl", hash = "sha256:f2e3d34edb151d3e27f93a8338e2da33e2c7261cd4c879ce5f5fe280cc190ea5", size = 504126, upload-time = "2026-10-05T17:41:46.321Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/90e9b6fd23310b4e82d0c0a37b5ecc2944053350473c57533c1da9647235/aiohttp-3.14.4-cp314-cp314t-win_amd64.whl", hash = "sha256:88cf889e51092537fe44adfbff96c2a22d6479d6f92b97b3d413fbfd0a4decf4", size = 531494, upload-time = "2026-10-05T17:41:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/5b501472c92b9b1dc53742591d6210dccb262962f4721299300d3a9f664b/aiohttp-3.14.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ca450c6d42fda7c0bfaf0e200f32f77c5f2c960a33be1e14d197c84588b7f5f6", size = 510544, upload-time = "2026-10-05T17:41:51.358Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/ef165a197850573ea07c8d77bf5f4854a1db12fdb20e1cf0b218abf025fc/aiohttp-3.14.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:3b10799df72f66c30935bc661cb89e5ac2918debfc575e350e226fbaa846cbb7", size = 540737, upload-time = "2026-10-05T17:41:53.761Z" }, + { url = "https://files.pythonhosted.org/packages/e6/aa/1663f64d1316b3be9d3f6fafa02b8d888070171a6544cba515e82f3af595/aiohttp-3.14.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:7ffd7f9dd39caccc6b2f68ec0339435ecad2b20f6ea2e2b3de91bbe464ac7137", size = 554600, upload-time = "2026-10-05T17:41:56.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/f7/d4d535855123c1a774a798643e75fdfbac62c047b0c0729d6505d09df444/aiohttp-3.14.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a0a7a227353f7693f73878ad14cd2623cafadd656ababcd20e2adfb7225172ea", size = 523184, upload-time = "2026-10-05T17:41:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/18a8d1cd2958a0b6164b047c18ccb192f2123bb70f1c0a145f6f01f858ad/aiohttp-3.14.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1c43b6af8e4708e8b3812a9ee4f79895207e12d555b7f6d70deb838e0011e12d", size = 530342, upload-time = "2026-10-05T17:42:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/d1dd2b1fcf8a3192a431375b287a8762ebfd22195ab5b201d3fac30e3a7a/aiohttp-3.14.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:97416b2b997e5e9b817e322b2f0a89b76c54cde22471f51ac17ac15e10901ba0", size = 540629, upload-time = "2026-10-05T17:42:03.324Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/d1868f3459f618f6b68a6ef549d8b3f76aa616fa083298911fd3ae65d6b9/aiohttp-3.14.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:ee2a5aa31e507c17841d592364f2148393b17f83bc2766e9211e1eb31ea72a70", size = 819347, upload-time = "2026-10-05T17:42:05.713Z" }, + { url = "https://files.pythonhosted.org/packages/84/e6/53179b721b67e0bd20974d02eca97b5691559e8ce666ba23a65aecda6b7f/aiohttp-3.14.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:5d5d9843c34866e986dd24eef713452609e304550c471320ce7b54b3cfb1391e", size = 549825, upload-time = "2026-10-05T17:42:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/00/0f/d67ddf7a43d4899176954511b6c048328abec1a5df4901f811a3b408a4fc/aiohttp-3.14.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:efcbf93e31b52c21665b6ebc489eb6fdb0dfd86b592edc677b5cffdfa7b11677", size = 549315, upload-time = "2026-10-05T17:42:10.766Z" }, + { url = "https://files.pythonhosted.org/packages/9d/db/42f372c7f8e259505f239cb49db75f23548deaec56d22c68b4866f1c3fe2/aiohttp-3.14.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f1a79977794592dcf6485ed964ec2448db527a1bc0c72ec89663658dad3a739", size = 1892181, upload-time = "2026-10-05T17:42:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/23/0d/8b1900216302a94f69b41bf0786bb992a6e67d703f4698e335c2442db615/aiohttp-3.14.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:515e7ac6d27509dc3ba2c35fb88970b00b9e7b07ee7cc947c8733dcd54ef5def", size = 1883513, upload-time = "2026-10-05T17:42:15.704Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/15c014313e567fff931ec4a5f81683cc40251257791fbd607ec37c0dad80/aiohttp-3.14.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:185154ffa3b54765b5de96f464f41cfa476f8815a03854ae5e0fe4d48fb06e9c", size = 1933283, upload-time = "2026-10-05T17:42:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b3/bf7dc39a450218efb1d5c692bac7dc85ef56da282e889bee594f3267211e/aiohttp-3.14.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6aece59f77959628a22cd2ffe48b102cda9611d680e090f204385b3998a1190f", size = 2041054, upload-time = "2026-10-05T17:42:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b6/03/ffba18d7aaedb5eafac3f41f0c26621ebb0a740bb0115ea2fae1911d902b/aiohttp-3.14.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7205aa6589f1ad1306c6994ff2e1ab5efabacf938f9ffd7682fb225c197b06a", size = 1895494, upload-time = "2026-10-05T17:42:23.518Z" }, + { url = "https://files.pythonhosted.org/packages/45/f4/68123365b86826eee82bdceb7fb4c429204cb3a104d6b7ae44bc6a543fcb/aiohttp-3.14.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f38c7b3ce60d55ac0f2a9fd2edc194789d043fefea15cfb7dcb40b7d5f561146", size = 1714098, upload-time = "2026-10-05T17:42:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/b13c0b214afc571782e8667f3b9e0bbed6843397be90fa35acf2f3ecfbb0/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b0a1274ee4ee6203c15a341cce87f9f3df8151d5496047b3608bb62bd969cffc", size = 1857111, upload-time = "2026-10-05T17:42:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/4f5d3cee3c23b57119aebba5174962fed0bc493f37f38edab2f5730411b6/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:69ce2407951516f413adf3132b88451eababd710ce631f83c81a9603a3640253", size = 1892123, upload-time = "2026-10-05T17:42:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/5a6a1f014c4e17d58d4e935d0dd64e612f4eaa2f33cca2475b48ca5e3fb5/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:b84d3d057c60eb8e0c2b457a1203e868d82709d3f0c272a4975dfd88c2414939", size = 1898809, upload-time = "2026-10-05T17:42:34.34Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/d1cca5311a70fcc1c7c2082e02044578a69bcb10686db33a322df9fb849e/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4ecc84f69c9e217984f6877e832e4786a74567432c0bf2b7512919fbdfe18ade", size = 1704814, upload-time = "2026-10-05T17:42:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/0a/cb/cd58eae366148a5b9b1baa233542151154f2b72cd32daa39fd9db97d0c0d/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:878f2d7950238b2b0a8cbb88054444a125f0cfbc77f54c811aca0483132a9be3", size = 1918390, upload-time = "2026-10-05T17:42:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/f5e6c4939432734033048ba631503edf4a1e693ee5e261773e169803bbe4/aiohttp-3.14.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6ff9d3a0e6935f1cb0a6685f9f4b3f0d3e406456d7a434a03cf309b8cd5d7a2e", size = 1878321, upload-time = "2026-10-05T17:42:42.528Z" }, + { url = "https://files.pythonhosted.org/packages/2b/71/462957d6b5d4eb208f78e67745c41480bad2e738aa7caf1d9b07536920c1/aiohttp-3.14.4-cp315-cp315-win32.whl", hash = "sha256:e07dfd7cd360f20be26dc2487ad4c7f21b0b39fdd593e9ebf37d2424ad52defb", size = 485536, upload-time = "2026-10-05T17:42:45.223Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6e/48f59266a8b8c6420894ff59b8e74175252da67df85967b96997e96b7092/aiohttp-3.14.4-cp315-cp315-win_amd64.whl", hash = "sha256:921fc4f1ad549091bfc39cb5e93925b04f307f83517756a3111843851130efce", size = 512087, upload-time = "2026-10-05T17:42:47.717Z" }, + { url = "https://files.pythonhosted.org/packages/25/f5/46263b23cee4f312e28aaabbdec2e074c807700a12c9dfa18beff461f035/aiohttp-3.14.4-cp315-cp315-win_arm64.whl", hash = "sha256:5be8af106f96fd625f6c264fa9faaaa9e0104089684d9015d2fa5b6eba6407c7", size = 494709, upload-time = "2026-10-05T17:42:50.615Z" }, + { url = "https://files.pythonhosted.org/packages/77/8d/e3b1d406c95a8fe436ae2e5f01ee9591196c6c22c44c645b24f20d73eaea/aiohttp-3.14.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e69727ec04a4608200ff32ec29478299df7657e2eb78c416ae338d0ca6662596", size = 856014, upload-time = "2026-10-05T17:42:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fc/e9e78d1097830040bc0a4b8af073379f964064bfa7f0a852d774f96f3168/aiohttp-3.14.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f8b658dd3ef2ebd7708b311ed96ff7cb5edaca001a30018a091504ef492bcb1c", size = 567724, upload-time = "2026-10-05T17:42:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/c813a705a1da1c111c30723b52e1efb67763e9f0b3b54a418cb419616e7a/aiohttp-3.14.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3749e018123df1205161a73ba96d336e17e9cf6b947295e53a43571de420e7b1", size = 567888, upload-time = "2026-10-05T17:42:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1f/7676f46aa24554a45686853c61552d02d67ade2b4c51adfe8b2ec4fdecd2/aiohttp-3.14.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f28a3c4fca436b2e594090e7cd22558f5e93a30ca18516c436de982da6fc25a", size = 2054356, upload-time = "2026-10-05T17:43:01.781Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/b9ef7d7442061e0a3125990d6021230b30361a972f7b8ebfb011042877ce/aiohttp-3.14.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e099aae21246992a1d84303ff262262d054c474e46cace33b0d5a69810c71877", size = 1937678, upload-time = "2026-10-05T17:43:04.492Z" }, + { url = "https://files.pythonhosted.org/packages/5c/98/5927a31540c8e6d0542effead5bd5c56e749a9d99d84e902fb80ae906ef8/aiohttp-3.14.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fafb521891e646d9cf5da89f01c75e7159d08be7254a024149630a698be28251", size = 2026705, upload-time = "2026-10-05T17:43:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/62/bd/1be001ed97dcfa57a292188c38979fb78be795b960b50f02660587bffa06/aiohttp-3.14.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d385db2cd154bce0b3dd451cf2a52bec91ebb7be2c3a5d06b14b4391631d807", size = 2147075, upload-time = "2026-10-05T17:43:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f7/d59628ccedd7c86db4c1f904467d1c80fed6cc10a1831ed5d75ff12e5a71/aiohttp-3.14.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae8a244889d8a53549851bf595b9f382f64acca57e8762da1ecb26ba786828f7", size = 2005016, upload-time = "2026-10-05T17:43:12.848Z" }, + { url = "https://files.pythonhosted.org/packages/3c/dc/472db3ab1daafa12a61d7f83314f85f502b99c143c78d66554a5e24361fd/aiohttp-3.14.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:681dde68ff8d8d5e7d5abcad4feb45bfb457a218310c919825e21fc36cac3d0b", size = 1796363, upload-time = "2026-10-05T17:43:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/61/6f9834b131b9a42e30d68c469c196033d3cae2d8fe340d4668f362c4fd1f/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b91a6441fcaed88ca7bca67726b189332df3ceb6ac0dfc98dcb9654bb723288c", size = 1974240, upload-time = "2026-10-05T17:43:18.884Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/b8eb2a276685ef1da7f7bc15348404b8b9ee12b8f32f2cd8895385cd1589/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f15dbbac49bc15eaff95c0b8166525c4050996af578c7be141e145f40ee2cf24", size = 1973387, upload-time = "2026-10-05T17:43:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/5b/32/ad559b2d6817e85f7cb294d8641e975e64b526f4da98ffe941c9af67a047/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7abae043e87692d1c2adda1fab501bb0612d28c4a23eb082005c1e684f792dac", size = 1968766, upload-time = "2026-10-05T17:43:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/60/67/4a16e398140bdabb363428b9f439af3f77706a6df8bea6e626bbde53c2ab/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:21f4624fb1051fc1aa10d57a636a80e829f7d0417462d6f1ce380b08fe1a904c", size = 1788193, upload-time = "2026-10-05T17:43:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/a9/38/17b326da7f52e530a675d789ade3e727a1b3db9959722f6049b9c392321a/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:2e8dc3a0fdaf9d12b954d3d06cc4c355d91b17fb6c671715b593624ca4c379f8", size = 1999645, upload-time = "2026-10-05T17:43:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/91b87961250010e05eb3adabaf0d98e4229ee7543147c265e834922e1b0b/aiohttp-3.14.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1410b6f6e3d52ea7c02c950496522ce6e85d5543bf154bc80a6141e505251cfa", size = 1970998, upload-time = "2026-10-05T17:43:33.364Z" }, + { url = "https://files.pythonhosted.org/packages/72/66/9bcf2c2db7b75a3b51c11470ceb67941d028727d674276ff5d5120adc87e/aiohttp-3.14.4-cp315-cp315t-win32.whl", hash = "sha256:a65189a89f3e621f7c27a968b5131bbca29126e2b8517dc72b205747a39d4c96", size = 504036, upload-time = "2026-10-05T17:43:36.212Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/349db4af12dbc16070d32028d30c2067123b4520f3dcb2d9013524efeb4f/aiohttp-3.14.4-cp315-cp315t-win_amd64.whl", hash = "sha256:2b2c95f5f769eec92b552969db1f79ea0282058ce5131ed0f2fa540b4af3f14e", size = 531284, upload-time = "2026-10-05T17:43:39.362Z" }, + { url = "https://files.pythonhosted.org/packages/26/21/544c711bc5fad611278ebd153bdcab1bffce8b8581f8ec9096b275948a3c/aiohttp-3.14.4-cp315-cp315t-win_arm64.whl", hash = "sha256:54209fff79346ee1ef0d5cbf92fa80a5bb37396406bb4d932e5107f9c29b2d3f", size = 510623, upload-time = "2026-10-05T17:43:42.15Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/5e1f8446545be98e9416af59c396b1ed1f9f3294afb0a40ee3a709889273/aiohttp-3.14.4-py3-none-any.whl", hash = "sha256:5c6758ba62aea282c537179cfc8474a90f2b5f7b089cc5ff66d8920d86a9bfdd", size = 279487, upload-time = "2026-10-05T17:43:44.905Z" }, ] [[package]] @@ -3382,88 +3424,101 @@ wheels = [ [[package]] name = "yarl" -version = "1.23.0" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] [[package]]