Skip to content

fix(ai-isolate-quickjs): bridge host tools with native promises#946

Closed
TheBoyWhoLivedd wants to merge 3 commits into
TanStack:mainfrom
TheBoyWhoLivedd:fix/quickjs-native-promise-bridge
Closed

fix(ai-isolate-quickjs): bridge host tools with native promises#946
TheBoyWhoLivedd wants to merge 3 commits into
TanStack:mainfrom
TheBoyWhoLivedd:fix/quickjs-native-promise-bridge

Conversation

@TheBoyWhoLivedd

@TheBoyWhoLivedd TheBoyWhoLivedd commented Jul 15, 2026

Copy link
Copy Markdown

🎯 Changes

Replace Asyncify-based host tool callbacks in @tanstack/ai-isolate-quickjs with native QuickJS promises returned from synchronous host functions.

The Asyncify bridge can corrupt a reused QuickJS context when guest code performs multiple sequential awaited host calls within one execution. Depending on the number of awaits and guest allocations, the failure can appear during that execution or during a later evaluation on the same context.

This change:

  • creates a synchronous QuickJS context;
  • returns a native QuickJS promise for asynchronous host tool work;
  • executes pending QuickJS jobs after the host promise settles;
  • allows independent host tool calls to run concurrently;
  • propagates pending-job failures and terminates timed-out contexts safely;
  • adds regression coverage for three sequential awaited calls and subsequent context reuse.

The failure is independently reproduced here:

https://github.com/TheBoyWhoLivedd/quickjs-emscripten-asyncify-multi-await-repro

Related upstream QuickJS work:

The TanStack change does not depend on the upstream patch. It avoids the affected Asyncify path and provides a safe implementation for currently published QuickJS versions.

A patch changeset for @tanstack/ai-isolate-quickjs is included.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability for repeated asynchronous tool calls within the QuickJS isolate runtime.
    • Fixed context reuse after tool executions, including more consistent handling of fatal limits and timeouts.
    • Improved coordination for concurrent tool invocations and ensured correct shutdown behavior after deadline failures.
  • Tests

    • Expanded tool-binding coverage for sequential tool calls with context reuse.
    • Added concurrency tests to validate the maximum number of active tool calls.
    • Added timeout tests, including follow-up failures after the isolate is disposed.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6b8fdd5-133d-4a61-9874-39530a6f3153

📥 Commits

Reviewing files that changed from the base of the PR and between b9a5632 and 626c882.

📒 Files selected for processing (3)
  • packages/ai-isolate-quickjs/src/isolate-context.ts
  • packages/ai-isolate-quickjs/src/isolate-driver.ts
  • packages/ai-isolate-quickjs/tests/isolate-driver.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ai-isolate-quickjs/tests/isolate-driver.test.ts
  • packages/ai-isolate-quickjs/src/isolate-driver.ts
  • packages/ai-isolate-quickjs/src/isolate-context.ts

📝 Walkthrough

Walkthrough

QuickJS isolate execution now uses synchronous contexts with native promise bridging for asynchronous host tools. Execution coordinates deadlines and terminal disposal, while tests cover repeated awaits, context reuse, concurrent calls, and timeout behavior.

Changes

QuickJS promise bridge

Layer / File(s) Summary
Synchronous QuickJS execution and disposal
packages/ai-isolate-quickjs/src/isolate-context.ts
QuickJSIsolateContext uses QuickJSContext, evaluates wrapped async code through evalCode, coordinates pending jobs and deadlines, and requests VM disposal after terminal failures.
Native promise host tool bridge
packages/ai-isolate-quickjs/src/isolate-driver.ts
Tool bindings use native QuickJS promises, JSON success/error results, host-call tracking, VM liveness checks, and pending-job execution instead of Asyncify functions.
Regression coverage and release metadata
packages/ai-isolate-quickjs/tests/isolate-driver.test.ts, .changeset/quickjs-native-promises.md
Tests cover sequential awaits, context reuse, concurrent Promise.all calls, timeout disposal, and the patch changeset records the update.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant QuickJSWrapper
  participant QuickJSBinding
  participant ToolBinding
  participant QuickJSRuntime
  QuickJSWrapper->>QuickJSBinding: invoke injected tool promise
  QuickJSBinding->>ToolBinding: execute parsed input
  ToolBinding-->>QuickJSBinding: return success or error JSON
  QuickJSBinding->>QuickJSRuntime: resolve promise and run pending jobs
  QuickJSRuntime-->>QuickJSWrapper: return value or throw error
Loading

Possibly related issues

Possibly related PRs

  • TanStack/ai#943: Refactors the same QuickJS tool-binding bridge and related execution tests.
  • TanStack/ai#948: Covers the corresponding synchronous evalCode/promise flow and timeout/disposal changes in the same QuickJS package.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing host tool bridging with native promises in ai-isolate-quickjs.
Description check ✅ Passed The description matches the template with Changes, Checklist, and Release Impact sections filled out and includes the needed changeset note.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai-isolate-quickjs/src/isolate-context.ts (1)

96-131: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle executePendingJobs() results in both QuickJS drains

  • packages/ai-isolate-quickjs/src/isolate-context.ts#L106: check the executePendingJobs() result and dispose any returned error handle instead of letting an interrupted drain leave await nativePromise hanging.
  • packages/ai-isolate-quickjs/src/isolate-driver.ts#L74: do the same here so the VM-side wakeup doesn’t swallow the error or leak the handle.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-isolate-quickjs/src/isolate-context.ts` around lines 96 - 131,
Handle the return value from executePendingJobs() in both
packages/ai-isolate-quickjs/src/isolate-context.ts lines 96-131 and
packages/ai-isolate-quickjs/src/isolate-driver.ts lines 54-100: detect
interrupted/error results, dispose any returned error handle, and propagate the
failure so awaiting the QuickJS promise cannot hang or silently leak resources.
🧹 Nitpick comments (1)
packages/ai-isolate-quickjs/src/isolate-driver.ts (1)

150-160: 🚀 Performance & Scalability | 🔵 Trivial

Single toolQueue serializes every host tool call for the whole context, not just per binding.

enqueue is created once per createContext call and shared across all bindings injected in the loop below (Line 205-207), confirmed by the "preserves one active host call for Promise.all" test. This means calling two independent tools concurrently (e.g. Promise.all([toolA(), toolB()])) is now fully serialized on the host side too, even though native promises (unlike Asyncify) no longer require a single in-flight suspension. If this broad serialization is intentional (e.g., for predictable ordering/log interleaving), it's worth documenting; otherwise, consider scoping the queue per binding to allow independent tools to execute concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-isolate-quickjs/src/isolate-driver.ts` around lines 150 - 160,
Scope the host-call queue to each injected binding instead of sharing the single
toolQueue created in createContext across all bindings. Move or create the queue
alongside each binding’s enqueue logic in the binding-injection loop, preserving
serialization for repeated calls to the same tool while allowing independent
tools to execute concurrently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/ai-isolate-quickjs/src/isolate-context.ts`:
- Around line 96-131: Handle the return value from executePendingJobs() in both
packages/ai-isolate-quickjs/src/isolate-context.ts lines 96-131 and
packages/ai-isolate-quickjs/src/isolate-driver.ts lines 54-100: detect
interrupted/error results, dispose any returned error handle, and propagate the
failure so awaiting the QuickJS promise cannot hang or silently leak resources.

---

Nitpick comments:
In `@packages/ai-isolate-quickjs/src/isolate-driver.ts`:
- Around line 150-160: Scope the host-call queue to each injected binding
instead of sharing the single toolQueue created in createContext across all
bindings. Move or create the queue alongside each binding’s enqueue logic in the
binding-injection loop, preserving serialization for repeated calls to the same
tool while allowing independent tools to execute concurrently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1acaa342-f4f1-4fa1-b079-223e3369eccc

📥 Commits

Reviewing files that changed from the base of the PR and between 5fcaf90 and 2b7c35e.

📒 Files selected for processing (4)
  • .changeset/quickjs-native-promises.md
  • packages/ai-isolate-quickjs/src/isolate-context.ts
  • packages/ai-isolate-quickjs/src/isolate-driver.ts
  • packages/ai-isolate-quickjs/tests/isolate-driver.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-isolate-quickjs/src/isolate-context.ts`:
- Around line 103-124: Update requestVmDispose and the host-call tracking in
beginHostCall so dispose does not wait indefinitely for pending host promises;
add cancellation or a bounded teardown fallback that guarantees vm.dispose()
eventually runs even when a host call never completes, while preserving the
existing immediate teardown when no calls are pending and preventing duplicate
disposal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09fcf589-23ab-4785-b52a-5e2d50cc5ad9

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7c35e and b9a5632.

📒 Files selected for processing (3)
  • packages/ai-isolate-quickjs/src/isolate-context.ts
  • packages/ai-isolate-quickjs/src/isolate-driver.ts
  • packages/ai-isolate-quickjs/tests/isolate-driver.test.ts

Comment thread packages/ai-isolate-quickjs/src/isolate-context.ts Outdated
@tombeckenham

Copy link
Copy Markdown
Contributor

Hi @TheBoyWhoLivedd , thank you for raising a PR to fix this issue. Another PR was raised just after yours - #948 which has a more complete solution to the problem. Whilst I'd usually take the first solution, it was so close in timing, I'd prefer to go with 948 as it's closer to done. I'd invite you to please review #948 as well and contribute anything you think needs changing. I'm closing this one

@TheBoyWhoLivedd

Copy link
Copy Markdown
Author

Thanks for the explanation. I’ll follow #948. I’m glad the teardown work from this PR was useful during its review.

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.

2 participants