docs(code-runtime-python): correct the ProtocolChannel serialization docstring

The class docstring still credited the GIL plus per-frame PIPE_BUF atomicity for
serializing writes, which _write_lock's full-write loop already superseded. State
the current contract (writers serialized by _write_lock around a full-write loop)
and drop the double blank line under the binding-replies note heading.
This commit is contained in:
Chinesezjc 2026-08-02 18:34:04 +08:00 • committed by Tianyi Cui
parent a8e47dae37
commit 9f449a79a6
3 changed files with 7 additions and 5 deletions

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md
2026-07-31-code-runtime-python-settlement-fixes.md: d276c04e0182dc589f48a77d8dd73b233ae11d9d
2026-07-31-code-runtime-python-settlement-fixes.md: b51fb2e9c28d07efa1691b1036b7b6e899b21062
2026-07-31-code-runtime-python-settlement-fixes.zh.md: a59e5457f2469e86ed4120ce47ed8a5d8ae08f4f

View file

@ -40,7 +40,6 @@ In [`py/bootstrap.py`](../../../../packages/code-runtime/code-runtime-python/py/
### Binding replies complete on the calling loop's thread
Also in `py/bootstrap.py`, a binding reply Future is created on the loop that ran `dispatch`. When the model calls a binding from a worker THREAD via `asyncio.run(tools.x(...))`, that Future belongs to the thread's loop, not the main loop where `_pump_replies` reads the reply. `asyncio.Future` is not thread-safe: completing it from another thread does not wake its own loop, so the direct `set_result`/`set_exception` left the awaiting thread stranded and the run degraded to a wall-clock timeout. Each pending entry now records its Future's loop alongside the Future, and `_pump_replies` completes it via that loop's `call_soon_threadsafe`. The shared `pending`/`next_id` state is guarded by a `threading.Lock` held across the id claim, the fd-3 write, and the counter advance, so concurrent callers cannot interleave frames out of the id order the host requires. `call_soon_threadsafe` onto a loop that has already CLOSED (the worker thread finished and abandoned its call before the reply arrived) raises `RuntimeError`; that schedule is wrapped so the moot reply is dropped rather than letting the exception end the pump task and strand every later reply.
## Testing

View file

@ -345,9 +345,12 @@ class ProtocolChannel:
Writes are unbuffered and go straight to the fd, so ``send_sync`` is safe
from inside model code (which may run outside an asyncio task) and from
background tasks alike. The single writer is serialized by CPython's GIL
plus one os.write per frame (POSIX guarantees atomicity for writes below
``PIPE_BUF``, and our frames are short JSON lines).
background tasks alike. Concurrent writers are serialized by ``_write_lock``
around a full-write loop (see ``send_sync``): ``os.write`` releases the GIL,
a frame may exceed ``PIPE_BUF`` (logs up to ``maxLogBytes``, completions up
to ``maxValueBytes``, uncapped ``call`` args), and one ``os.write`` may
consume only part of a frame — so neither the GIL nor per-frame atomicity is
relied on for framing.
"""
def __init__(self, fd: int) -> None: