Skip to content

fix(sdk): watch handle callback awaiting, request timeouts, and exit semantics#1436

Draft
mishushakov wants to merge 6 commits into
mainfrom
mishushakov/fix-watchhandle-callbacks-timeout
Draft

fix(sdk): watch handle callback awaiting, request timeouts, and exit semantics#1436
mishushakov wants to merge 6 commits into
mainfrom
mishushakov/fix-watchhandle-callbacks-timeout

Conversation

@mishushakov

@mishushakov mishushakov commented Jun 12, 2026

Copy link
Copy Markdown
Member

Description

Fixes several WatchHandle issues across the SDKs and aligns the watch lifecycle contract between JS and Python:

  • JS: async onEvent/onExit callbacks are now awaited (sequential event handling, callback errors routed to onExit).
  • JS: stop() reports a clean exit instead of a misleading TimeoutError, and now resolves only after the watching has fully ended and onExit has completed — re-throwing errors raised by onExit. An in-flight onEvent callback is abandoned on stop (the JS equivalent of Python's cancelled handler task), which also prevents a deadlock when stop() is awaited from inside onEvent.
  • Python (sync): get_new_events()/stop() now apply the default request timeout and the user auth headers (previously unbounded and sent as the default user) and accept a request_timeout parameter.
  • Python (async): on_exit is now invoked whenever the watching ends — with the exception on stream failure, or None on normal stream end and on stop() — and stop() re-raises exceptions from on_exit instead of silently swallowing them.

The resulting contract is identical in both SDKs: the exit callback fires exactly once however the watching ends (None/undefined on normal end or user stop, the error otherwise), and stop() returns only after exit handling has completed.

Includes unit tests for all three handles, a secured-envd watchDir test for JS, and a fix for the async secured-envd test which wasn't actually using a secure sandbox.

Usage

// JS: async callbacks are now awaited; stop() reports a clean exit
const handle = await sandbox.files.watchDir('/home/user', async (event) => {
  await processEvent(event) // completes before the next event is handled
}, {
  onExit: (err) => {
    // err is undefined on normal stream end or stop(), the error otherwise
  },
})
await handle.stop() // resolves after watching fully ended and onExit completed
# Python (async): on_exit now fires on every end of watching
handle = await sandbox.files.watch_dir(
    "/home/user",
    on_event=lambda e: print(e),
    on_exit=lambda err: print("watch ended:", err),  # None on normal end/stop()
)

# Python (sync): polling calls now honor timeouts and user auth
handle = sandbox.files.watch_dir("/home/user", user="root")
events = handle.get_new_events(request_timeout=10)  # runs as "root", times out
handle.stop()

🤖 Generated with Claude Code

- JS: await async onEvent/onExit callbacks in WatchHandle so events are
  handled sequentially and callback errors are routed to onExit
- JS: report a clean exit (no error) to onExit when the watch is stopped
  via stop() instead of a misleading TimeoutError
- Python sync: apply request timeout and user auth headers to
  WatchHandle.get_new_events/stop and accept a request_timeout param
- Python async: invoke on_exit whenever the watching ends — with the
  exception on stream failure, or None on normal stream end and stop()
- Tests: unit tests for all three handles, secured-envd watch coverage
  for JS, fixed async secured-envd test to actually use a secure sandbox

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f48d20

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Patch
@e2b/python-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Jun 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes async watch callback ordering and stop/exit semantics in both SDKs, which can affect apps relying on prior fire-and-forget behavior or misleading stop errors; sync Python watch now uses correct auth headers (behavior fix for secured sandboxes).

Overview
Aligns filesystem WatchHandle lifecycle across JS and Python and fixes several edge-case bugs.

JS WatchHandle now awaits async onEvent/onExit, routes onEvent failures to onExit, and treats user stop() as a clean exit (undefined) instead of a stream abort error. stop() waits until watching ends and onExit finishes, rethrows onExit errors, and abandons in-flight onEvent (including stop() from inside onEvent) to avoid deadlocks.

Python async WatchHandle matches that contract: on_exit runs once on normal end, stop(), or stream failure (None vs exception), and stop() propagates on_exit errors instead of swallowing them.

Python sync WatchHandle get_new_events() / stop() now send auth headers and connection request timeouts (optional request_timeout), wired from watch_dir(..., user=...).

JS start-stream helpers throw Expected start event when the first event is missing or wrong (replacing a TypeError on empty streams).

Adds unit tests for handles and start events, plus secured-envd watch coverage (JS integration; Python async test fixed to use a secure sandbox).

Reviewed by Cursor Bugbot for commit 4f48d20. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 9107a22. Download artifacts from this workflow run.

JS SDK (e2b@2.30.1-mishushakov-fix-watchhandle-callbacks-timeout.0):

npm install ./e2b-2.30.1-mishushakov-fix-watchhandle-callbacks-timeout.0.tgz

CLI (@e2b/cli@2.11.2-mishushakov-fix-watchhandle-callbacks-timeout.0):

npm install ./e2b-cli-2.11.2-mishushakov-fix-watchhandle-callbacks-timeout.0.tgz

Python SDK (e2b==2.29.0+mishushakov-fix-watchhandle-callbacks-timeout):

pip install ./e2b-2.29.0+mishushakov.fix.watchhandle.callbacks.timeout-py3-none-any.whl

Comment thread packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfb9c5bd8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/js-sdk/src/sandbox/filesystem/watchHandle.ts Outdated
- JS: report onEvent callback errors to onExit even when stop() was
  requested while the callback was in flight — only the stream abort
  caused by stop() is treated as a clean exit
- Python async: stop() now re-raises exceptions raised by on_exit
  instead of silently swallowing them via asyncio.wait

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py
stop() now resolves only after the watching has fully ended and the
onExit callback has completed, re-throwing errors raised by onExit.
An in-flight onEvent callback is abandoned on stop — the JS equivalent
of the cancelled handler task in the Python SDK — which also prevents
a deadlock when stop() is awaited from inside onEvent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishushakov mishushakov marked this pull request as draft June 12, 2026 21:09
@mishushakov mishushakov marked this pull request as ready for review June 12, 2026 21:23
Guard against an empty process/watch start stream where `.next().value`
is `undefined`. The optional chain was on `.event` rather than the
`startEvent` itself, so an exhausted stream slipped past the guard and
surfaced as a bare `TypeError: Cannot read properties of undefined`
instead of the intended `Expected start event` error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/js-sdk/src/sandbox/filesystem/watchHandle.ts
mishushakov and others added 2 commits June 15, 2026 15:37
…t stop

Promise.race could pick the stop over an onEvent callback that rejected in
the same tick, swallowing the error and calling onExit(undefined). Track the
callback's settlement so a settled (resolved/rejected) callback always wins
over a concurrent stop(); only a still-pending callback is abandoned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…handle-callbacks-timeout

# Conflicts:
#	packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py
#	packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
#	packages/python-sdk/e2b/sandbox_sync/filesystem/watch_handle.py

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4f48d20. Configure here.

this.stopped = true
this.notifyStopped()
this.handleStop()
await this.handlingEvents

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reentrant stop deadlocks event loop

High Severity

WatchHandle.stop() now awaits handlingEvents, so calling stop() (or await stop()) from a synchronous onEvent handler or from onExit while handleEvents is still inside those callbacks deadlocks: stop() waits on handlingEvents, which cannot finish until the callback returns.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f48d20. Configure here.

@mishushakov mishushakov marked this pull request as draft June 15, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant