Repository navigation
refactor(codemode): run programs on one job queue instead of a fiber per async call - #57976
rekram1-node wants to merge 4 commits into
Conversation
… bound retained reactions
|
Context: I have an open PR (#54976) that adds one path inside this same Two things from reading An Effect global now falls into the blocking path. With the branch absent, If that path is wanted later, That last point is not theoretical. On current So: do you want the Effect-global capability in the new runner at all? If yes I will rebase #54976 onto |
Fixes #54340.
Problem
Code Mode ran every async function call, promise, and generator step as its own Effect fiber, and a turn token (#54276) forced those fibers to take turns running program code. That worked, but:
finally, promise adoption, empty combinators, async generators,for awaitover arrays).await new Promise(() => {})waited for the timeout, and core passes no timeout, so it waited forever (codemode: execute hangs until eviction when a program awaits a promise nothing can settle #54340).Change
The evaluator is now plain generator functions driven by one job queue per execution. Effect stays at the boundary:
execute(), the timeout, interruption, tools, hooks, and extensions.const r = await tools.search({ q }) - fork a fiber for the tool promise - suspend this fiber; the turn token passes to the next waiting fiber - Fiber.await, retake the turn, resume + start the tool as an Effect fiber (host work, as before) + yield Await: split the stack at the async function's boundary, park its frames on the promise + run the next job from the queue + tool settles: queue a job that resumes the parked framesExecution stalled: ....then,finally, combinators, async generators, and async-from-sync iteration follow the spec's job structure.i++, assignments, member reads, and built-in calls that return a value directly skip generator allocation.CallSitecontext reference.effectvalue imports insrc/interpreter/**andsrc/stdlib/**except inrunner.ts,execute.ts, andextensions.ts.How to review: read
runner.tsfirst. Most of theinterpreter.tsand stdlib diff is mechanical (Effect.gen→function*,Effect.ensuring→try/finally,Effect.fail→throw);git diff -wshows +2,720/−2,124 instead of +3,593/−2,997.Results (Apple M5, best of 18 runs,
dev→ this branch)fib(22).thenchainBehavior differences
.then()chain now fails with "Execution exceeded the maximum nesting depth." (V8 fails it too). Program recursion is unchanged at 10,000 calls.Isolation audit
Three independent reviews compared this branch with
devfor host-reference leaks, mutation outside the execution, and confinement:devin 26 cases.Iterator.from({ next: Object }).drop(Infinity).next()) never reached a slice, so the timeout could not fireawait Promise.race([pending, 0])in a loop left one reaction per race on the pending promiseGenerator.prototype.next.call(asyncGenerator)hit an internal errorPre-existing findings, unchanged by this PR: program regexes update the host's legacy
RegExp.$1/lastMatch, and extension results with overriddenmap/slice/getTime/message/namecan bypass copying.Out of scope
CodeMode.make; CPU-bound loops still need one.Verification
packages/codemode:bun typecheck: clean.bun test: 14,786 pass, 0 fail (+25 test262 files removed fromskipped.txt, +29 V8-ordering cases, +12 new tests for stalls, host-result order, interruption, host responsiveness, deep nesting, timeouts through built-in loops, value walks, and rejection reporting, and generator protocol misuse).dev), with no file regressing.bun run check: 34/34 tasks.packages/core:bun test test/codemode.test.ts test/codemode test/tool-execute.test.ts test/tool-registry.test.ts test/mcp.test.ts test/session-runner.test.tsgives 369 pass, 0 fail.while (true) {}and endless promise-job streams within about 10 ms of the limit.dev).