fix(sandbox): settle PTY output before cleanup - #4738
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21c32b9985
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
21c32b9 to
41be368
Compare
fscfede-beep
left a comment
There was a problem hiding this comment.
I re-audited the current 41be368c head specifically around cancellation ownership. Two cancellation boundaries can still lose bytes that this PR now intends to carry across PTY windows.
|
Cross-link for collision/maintainer coordination: #4745 was opened later against the same PTY cross-window UTF-8 / settlement area and has since accumulated a different implementation plus several cancellation/finalization fixes. I’m reviewing both rather than opening a third implementation. The two current cancellation findings I left on this PR ( |
|
Selective salvage after comparing #4745 against current One test idea is still useful here: a controlled Unix lifecycle regression where I would add that regression only after/alongside the two cancellation ownership fixes already posted in |
|
hi @seratch. i had a pull request in this area, #4745, which i have just closed in favour of this one. it predates mine and covers the same ground, so there is nothing to weigh up there. @fscfede-beep reviewed both and suggested one thing from mine might still be worth having, so i am leaving it here as an idea rather than a patch. take it or ignore it. this pr has the producer drained entry = _UnixPtyProcessEntry(process=process, tty=True) # returncode 0
# output_closed deliberately NOT set, the pump still holds the continuation
entry.output_chunks.append("é".encode()[:1])
# first update must leave the session alive with the lead byte still queued
assert first.process_id == 1
assert first.exit_code is None
assert list(entry.output_chunks) == ["é".encode()[:1]]
# then the pump delivers and closes
entry.output_chunks.append("é".encode()[1:])
entry.output_closed.set()
assert final.output.decode("utf-8") == "é"
assert final.process_id is Nonewhat made me think it earns its place is that it fails for the right reason. swapping the predicate back to so it catches a future tidy up that undoes the predicate, which is a one line change that reads like cleanup. the full version is in the closed branch at happy to open it as a small test only pull request against this branch if you want it, or to leave it entirely. no need to reply if not. |
|
@seratch, short follow up to my note above and then i will stop. the two cancellation findings @fscfede-beep raised on this pr are the same two i hit and fixed on the branch i closed, so there is working tested code for both if it saves you deriving them again. commits are on shared collector, except asyncio.CancelledError:
if output:
output_chunks.appendleft(bytes(output))
raisemodal, each has a regression that fails on the commit before it. the modal one consumes the continuation off the stream, cancels on the next await, and asserts both halves are still entry owned. one thing worth flagging, because it bit me right after i fixed these. once the tail flush moved in front of the registry pop, cancelling during the drain skipped that plus the blocked pump test above is everything i have. no reply needed, and i am not opening anything here. |
41be368 to
2b2d175
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b2d1750cf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ErenAta16
left a comment
There was a problem hiding this comment.
Flagging the neighbourhood rather than the diff, since three open PRs are editing
these files at once and only one of the three overlaps is a real duplicate.
@Hughhhhcoder's #4750 and @mikemikimike's #4751 both landed the day after this one.
Reading the source changes, the three are addressing different defects:
#4738 threads output_closed through _collect_pty_output output settled before cleanup
#4750 except Exception -> BaseException in pty startup fds leak on cancellation
#4751 wraps _terminate_pty_entry in a cancellation shield teardown aborts on cancellation
So they are complementary, and none of them subsumes another. All three touch
sandbox/sandboxes/unix_local.py, at lines 354, 391/463 and 400/442 respectively,
which is close enough to be worth knowing about but far enough apart that the
hunks should not fight.
The real duplicate is the helper, not the call sites. #4750 adds
_settle_pty_cleanup as a module function in sandbox/session/pty_types.py, and
#4751 adds _settle_pty_cleanup as a method on BaseSandboxSession in
sandbox/session/base_sandbox_session.py. Same name, same shield-in-a-loop
algorithm, same completion.result() then task.result() sequence. Merging both
leaves the SDK with two of them.
They are not equivalent, and the difference shows up when cleanup itself fails
while the caller is being cancelled:
cleanup raises, caller cancelled
#4750 -> CancelledError propagates task.cancelled() = True
#4751 -> RuntimeError propagates task.cancelled() = False
#4750 gives the caller's cancellation priority over the cleanup error; #4751
reaches task.result() before its caller_cancellation check, so the cleanup
exception wins and the CancelledError is dropped. A task that was asked to stop
and then reports cancelled() is False is the case wait_for and TaskGroup
both rely on, so I would take #4750's ordering whichever module the helper ends
up living in.
One completeness note that may save a round trip on #4750: the
except Exception -> except BaseException change there is the only site of its
shape. I walked the AST of everything under sandbox/ and
extensions/sandbox/, looking for a try whose body contains an await and
whose sole handler is except Exception while closing a descriptor, and
unix_local.py:357 is the single match. entries/artifacts.py:714 looks similar
but its try body is fully synchronous, so Exception is sufficient there and it
is not an outlier.
Co-authored-by: Kazuhiro Sera <seratch@openai.com> Co-authored-by: Henry Su <henrysu4707@gmail.com> Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>
2b2d175 to
62c03d7
Compare
This pull request fixes PTY output settlement and supersedes #4572 and #4724. PTY collectors now re-drain output at timeout boundaries, carry only complete valid UTF-8 sequences across read windows, and make bounded replacement progress for invalid E0, ED, F0, and F4 prefixes.
Terminal cleanup now follows a collector-owned settled
output_closedfact across local, Docker, E2B, Cloudflare, Modal, Blaxel, and Daytona adapters, so exit visibility cannot drop queued bytes or carried suffixes.