Skip to content

codemode: concurrent async functions lose updates to shared variables #54274

Closed

Description

@RhysSullivan

Description

In @opencode/codemode, concurrent async functions lose updates to shared variables, and a caller can continue before an async function's body reaches its first await.

The interpreter runs each async call, promise reaction and generator body on its own Effect fiber, and Effect yields a fiber after 2,048 operations. When several program fibers are ready at once (for example after the tool calls in a Promise.all return), their statements interleave mid-expression: total = total + 1 reads total, another fiber runs and writes it, and the first fiber's write drops those updates. JavaScript runs one job at a time, so both programs below are deterministic natively.

We hit this with programs that fold tool results from Promise.all into shared counters and strings: rows were dropped without any error.

Present on v2 at 55dde88 and in @opencode/[email protected].

Plugins

None

OpenCode version

@opencode/[email protected] (v2 at 55dde88)

Steps to reproduce

  1. In an empty directory: echo '{"type":"module"}' > package.json && npm i @opencode/[email protected] [email protected]
  2. Save as repro.mjs:
import { CodeMode } from "@opencode/codemode"
import { Effect } from "effect"

const programs = {
  // Two async functions add to a shared counter after an await.
  counter: `
    let total = 0
    const work = async () => {
      await null
      for (let i = 0; i < 1000; i++) total = total + 1
    }
    await Promise.all([work(), work()])
    return total
  `,
  // An async function's body runs synchronously up to its first await.
  prefix: `
    let count = 0
    const work = async () => {
      for (let i = 0; i < 1000; i++) count = count + 1
      await null
    }
    work()
    return count
  `,
}

const AsyncFunction = (async () => {}).constructor
for (const [name, code] of Object.entries(programs)) {
  const node = await new AsyncFunction(code)()
  const result = await Effect.runPromise(CodeMode.execute({ tools: {}, code }))
  console.log(`${name}: node ${node}, codemode ${result.value}`)
}
  1. node repro.mjs (Bun prints the same)

Expected:

counter: node 2000, codemode 2000
prefix: node 1000, codemode 1000

Actual (every run):

counter: node 2000, codemode 1853
prefix: node 1000, codemode 145

Screenshot and/or share link

No response

Operating System

Ubuntu 24.04 (Linux 6.8); Node 24.21.0, Bun 1.4.2

Terminal

No response

Sent from my Claude

Activity

  1. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    Thanks for the clear repro. I can confirm this.

    I ran your script against @opencode/[email protected], and also from source on the latest v2 (55dde88). Both gave the same numbers every run:

    counter: native 2000, codemode 1853
    prefix: native 1000, codemode 145
    

    So two things happen:

    • Concurrent async functions lose updates to shared variables.
    • A caller can continue before the async function's body has run up to its first await.

    The script below reproduces both. I've passed this on to the maintainers.

    Reproduction script (repro.mjs)
    // Run from a directory with @opencode/codemode and effect installed (or set CODEMODE to a path/specifier).
    const { CodeMode } = await import(process.env.CODEMODE ?? "@opencode/codemode")
    const { Effect } = await import("effect")
    
    const programs = {
      counter: `
        let total = 0
        const work = async () => {
          await null
          for (let i = 0; i < 1000; i++) total = total + 1
        }
        await Promise.all([work(), work()])
        return total
      `,
      prefix: `
        let count = 0
        const work = async () => {
          for (let i = 0; i < 1000; i++) count = count + 1
          await null
        }
        work()
        return count
      `,
    }
    
    const AsyncFunction = (async () => {}).constructor
    for (const [name, code] of Object.entries(programs)) {
      const node = await new AsyncFunction(code)()
      const result = await Effect.runPromise(CodeMode.execute({ tools: {}, code }))
      console.log(`${name}: native ${node}, codemode ${result.value}`)
    }

    Run it: bun repro.mjs

  2. rekram1-node commented on Oct 20, 2026

    @rekram1-node
    Collaborator

    oh hello there mr rhys

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions