Skip to content

[ai-isolate-quickjs] Two sequential tool calls crash the WASM module (Assertion failed: p->ref_count == 0) #947

Description

@SilvioYMollov

TanStack AI version

@tanstack/ai@0.40.0 / @tanstack/ai-isolate-quickjs@0.1.45

Framework/Library version

None — plain Node.js v22.21.1 (no framework; the bug reproduces in a standalone Node script using only @tanstack/ai-isolate-quickjs and @tanstack/ai-code-mode)

Describe the bug and the steps to reproduce it

Which project does this relate to?

@tanstack/ai-isolate-quickjs (Code Mode QuickJS isolate driver)

Description

If sandboxed code awaits a host tool binding more than once, the QuickJS WASM module crashes with a refcount assertion:

RuntimeError: Aborted(Assertion failed: p->ref_count == 0, at: ../../vendor/quickjs/quickjs.c,5512,free_zero_refcount)

The error can't be caught in practice, and it breaks the shared asyncify WASM module — so every other context in the process stops working too.

Code Mode is built around programs that chain multiple tool calls, so this makes the driver unusable for anything beyond a single tool invocation.

There's a related problem: if a tool binding returns a promise that never settles, execute() hangs forever. The configured timeout never fires, because it only exists as a QuickJS interrupt handler — there's no host-side timer running while the guest sits suspended in an asyncified call.

The root cause is that tool bindings are injected with vm.newAsyncifiedFunction() on a newAsyncContext() VM. Asyncify pauses and resumes the WASM stack on every host await, and doing that repeatedly corrupts QuickJS refcounts — a known upstream bug: justjake/quickjs-emscripten#258.

Platform

  • OS: Windows 11 (also happens on Linux/macOS and in StackBlitz WebContainers — the bug is in the WASM module, not the platform)
  • Node.js: v22.21.1
  • @tanstack/ai-isolate-quickjs: 0.1.45
  • quickjs-emscripten: 0.31.0

Suggested fix

Stop using asyncify. Instead of newAsyncContext() + newAsyncifiedFunction(), use the plain sync WASM build (getQuickJS().newContext()) and turn ea host function that returns a QuickJSpromise (vm.newPromise()). The host resolves that promise when the tool finishes, then calls runtime.executePendingJobs() so the gue. The WASM stack never gets suspended, so the upstream bug can't happen. This also makes two other things possible: the global cross-context execution queue can go away (it only exists becau one suspension at a time), andevalCodeAsync becomes plain evalCode + resolvePromise.

I have a working patch for this (it als the context, since with promise bridging an interrupted guest could otherwise be left waiting forever) and am happy to open a PR. The patch is published as a fork so it's easy to verify: @silviomollov/ai-isolate-quickjs (https://www.npmjs.com/package/@silviomollov/ai-isolate-quickjs) — swap only the import in the repro and it passes.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

https://stackblitz.com/edit/node-qwj94pnm?file=repro.mjs

Screenshots or Videos (Optional)

No response

Do you intend to try to help solve this bug with your own PR?

Yes, I am also opening a PR that solves the problem along side this issue

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions