Skip to content

fix(sdk-python): keep the async PTY session alive when a caller cancels wait() - #353

Open
TanishGudise wants to merge 1 commit into
daytona:mainfrom
TanishGudise:fix/async-pty-wait-cancellation
Open

TanishGudise wants to merge 1 commit into
daytona:mainfrom
TanishGudise:fix/async-pty-wait-cancellation

Conversation

@TanishGudise

@TanishGudise TanishGudise commented Oct 9, 2026 •

Copy link
Copy Markdown

Description

AsyncPtyHandle.wait() awaited the handle's long-lived WebSocket reader task directly. In asyncio, cancelling a coroutine that awaits a task also cancels that task, so bounding the wait with asyncio.wait_for() or asyncio.timeout() cancelled the reader when the timeout fired. After that:

  • on_data stopped firing
  • is_connected() returned False, and send_input() raised "PTY is not connected"
  • the WebSocket was left open
  • a later wait() raised CancelledError

This change awaits asyncio.shield(self._wait), so cancelling wait() only stops that wait. disconnect() still cancels the reader directly, so teardown is unchanged. The wait() docstring now notes the behavior.

Test: TestAsyncPtyHandleWaitCancellation in sdk-python/tests/test_pty_handle.py connects through AsyncProcess.connect_pty_session to a local aiohttp WebSocket server that stays silent until the client writes, so asyncio.wait_for(handle.wait(), 0.05) can only time out. The test then checks that:

  1. the handle is still connected;
  2. ping gets a pong back;
  3. after exit, wait() returns exit code 0.

On main it fails at the is_connected() check; with this change it passes on Python 3.10 and 3.12.

Documentation

  • This change requires a documentation update
  • I have made corresponding changes to the documentation

The AsyncPtyHandle.wait() docstring is updated. AsyncPtyHandle isn't part of the generated SDK reference docs, so nothing needs regenerating.

Related Issue(s)

Fixes #351

Screenshots

N/A

Notes

  • Open PR feat(sdk): add process surface with handles, log replay and reconnect #191 also edits async_pty_handle.py and test_pty_handle.py but doesn't change wait(), and this bug still reproduces on that branch. The wait() change applies cleanly on top of feat(sdk): add process surface with handles, log replay and reconnect #191; the test file only conflicts in its import block, which is a trivial union.
  • Checked locally:
    • yarn lint:py (0 errors) and the pre-commit lint-staged hooks
    • the Python suite on 3.12: 709 passed
    • the new test, 25 consecutive runs
  • test_event_subscription_manager.py::TestSyncEventSubscriptionManagerThreadUsage::test_many_subscriptions_share_one_expiry_thread fails intermittently on main too (a thread-count assertion; 2 of 8 local runs). It's unrelated to this change.
  • AI assistance: I developed this with help from Claude (Anthropic's AI assistant), which helped me find the bug, write the fix and test, and draft this description. I reviewed the changes and test results before opening the PR.

🤖 Generated with Claude Code


Summary by cubic

Fixes cancelling AsyncPtyHandle.wait() so a asyncio.wait_for() or asyncio.timeout() bound no longer kills the PTY session.

wait() previously awaited the WebSocket reader task directly, so cancelling it stopped on_data callbacks, made is_connected() return False, caused send_input() to raise "PTY is not connected", and left the WebSocket open. Now wait() awaits asyncio.shield(self._wait), so cancelling only stops the wait; disconnect() still tears down the reader as before.

Adds a regression test covering the timeout path, connection health after cancellation, and a successful exit afterwards.

Written for commit 0686475. Summary will update on new commits.

View guided diff

…ls wait()

AsyncPtyHandle.wait() awaited the handle's long-lived WebSocket reader task
directly. In asyncio, cancelling a coroutine that awaits a task also cancels
that task, so bounding the wait with asyncio.wait_for() or asyncio.timeout()
cancelled the reader itself when the timeout fired: on_data stopped firing,
is_connected() turned False, send_input() raised "PTY is not connected", the
WebSocket was left open, and a later wait() raised CancelledError.

Shield the reader task in wait() so cancelling the call only stops waiting.
disconnect() still cancels the reader directly.

Fixes daytona#351

Signed-off-by: Tanish Gudise <174986239+TanishGudise@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vidoc-agent

vidoc-agent Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Vidoc security review

Tip

Good to merge — no security issues found. Reviewed 2 changed files.

💬 Have questions? Tag @vidoc in a comment and I'll answer.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. ✅ Thank you!
Posted by the CLA Assistant Lite bot.

@TanishGudise

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

View guided diff | Re-trigger cubic

@TanishGudise

Copy link
Copy Markdown
Author

recheck

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sdk-python: a timed-out or cancelled AsyncPtyHandle.wait() kills the PTY session

1 participant