Skip to content

Fix #5: Workers AI streamed tool calls, Node.js module stubs for named imports; p11f queue hand-over timing - #9

Merged
mkotelnikov merged 3 commits into
mainfrom
ccr-ab3c0d30-6du8do-ai-stream-tools
Oct 27, 2026
Merged

mkotelnikov merged 3 commits into
mainfrom
ccr-ab3c0d30-6du8do-ai-stream-tools

Conversation

@mkotelnikov

@mkotelnikov mkotelnikov commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Two defects in #5, found reviewing it after it went up, and the cause of the queue test that kept failing in CI. Neither defect affects Flue's path (its provider reads the raw response, and only the default import of the stubbed modules), which is why #5's tests passed.

1. Streamed tool calls came out in pieces

env.AI.run(model, { messages, tools, stream: true }) passed each OpenAI chunk on by itself. OpenAI-compatible servers stream a tool call in fragments: the name first, then pieces of the arguments' JSON, all keyed by index. So a caller got several half calls, with empty names and unparsed fragments.

The stream now gathers the fragments by index and sends the whole calls once, before data: [DONE]:

data: {"response":"","tool_calls":[{"name":"weather","arguments":{"city":"Paris"}},{"name":"time","arguments":{}}]}
data: [DONE]

2. Named imports from a stubbed Node.js module were undefined

The stub for a module without a browser polyfill (node:child_process, node:readline, …) was CommonJS, and it created its members only when they were read. esbuild imports a CommonJS module by copying its own properties once, so it found none. import { spawn } from "node:child_process" gave undefined, and only mod.member threw as intended.

The stub is now an ES module:

  • it exports a throwing function for every export the module has on the Node.js that runs the bundler (classes can be extended at load, as libraries do);
  • its default export answers any member the same way, including Object.prototype's names.

3. The flaky "Queues: a delayed message sent on cellb is delivered by celld"

This test failed on #5 and #7, in e2e/p11f-celld-one-bucket.test.ts.

  • Cause: celld wakes cells from its wake index by UTC minute (wake/entries/<minute>/…). A message falling due in a later minute than the one celld started in is woken at celld's next scan, up to a minute later. The test polled for 20 s.
  • Not a cellb defect: cellb writes exactly the entry celld writes for its own delayed messages (checked against celld 0.6.3).
  • Reproduction: sending at :54 of a minute fails every time; the message arrives 59 s after celld starts.
  • Fix: this direction now polls for up to 80 s, with a 150 s timeout. It passed three runs timed to cross a minute boundary.

Tests

  • test/p14-workers-ai.test.ts:
    • streamed tool-call fragments become the whole calls (it failed before: six events of half calls);
    • a bundle importing node:child_process and node:readline gets named, namespace and default members that throw, and extends a stubbed class at load (it failed before: f is not a function).
  • Results on the branch, with main (48f6222) merged in:
    • Biome and tsc pass;
    • cellb unit suite: 335 passed;
    • cellb Chromium suite: 206 passed, 15 skipped;
    • p11f queue hand-overs against celld: 4 passed;
    • flue-example: 6 passed on Node, 8 passed in the browser.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SXJQA3LbggJHi5qZBpUKe8

…named imports

Two defects in #5:

- env.AI.run(…, { stream: true, tools }) passed each OpenAI chunk on by
  itself, so a tool call streamed in fragments (a name, then pieces of the
  arguments' JSON, keyed by index) came out as several half calls, with
  empty names and unparsed fragments. The stream now gathers the fragments
  by index and sends the whole calls once, before [DONE], as
  { response: "", tool_calls: [{ name, arguments }] }.
- The stubs for Node.js modules without a browser polyfill were CommonJS
  whose members were made on first read; esbuild copies a CommonJS module's
  own properties once, so named imports (`import { spawn } from
  "node:child_process"`) were undefined, and only `mod.member` threw as
  meant. The stub is now an ES module exporting a throwing function for
  every export the module has on the bundling Node.js (classes extend
  them), with a default export that answers any member (Object.prototype's
  names included) the same way.

Tests (p14): streamed tool-call fragments become the whole calls; a bundle
importing node:child_process and node:readline gets throwing named,
namespace and default members, and extends a stubbed class at load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXJQA3LbggJHi5qZBpUKe8
"Queues: a delayed message sent on cellb is delivered by celld" failed now
and then in CI (#5, #7). celld wakes cells from its wake index by minute
(wake/entries/<UTC minute>/…): a message due in a later minute than celld
started in is woken at its next scan, up to a minute on, and the test
polled for 20 s. Reproduced every time by sending at :54 of a minute
(delivered 59 s after celld started); cellb writes the same entry celld
writes for its own delayed messages. The test now polls for up to 80 s
(timeout 150 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXJQA3LbggJHi5qZBpUKe8
@mkotelnikov mkotelnikov changed the title Fix #5: Workers AI streamed tool calls, and Node.js module stubs for named imports Fix #5: Workers AI streamed tool calls, Node.js module stubs for named imports; p11f queue hand-over timing Oct 27, 2026
@mkotelnikov
mkotelnikov merged commit e4eb499 into main Oct 27, 2026
1 check passed
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