Skip to content

codemode: execute hangs until eviction when a program awaits a promise nothing can settle #54340

Closed

Description

@laurisvr

Description

A Code Mode execute program that awaits a promise nothing can settle never finishes. The interpreter has no timers, so a promise settles only through a tool or extension call, or through the program's own code. Once neither is running, nothing can ever settle it. execute has no time limit either, so the session holds its turn indefinitely. After 60 minutes the location's inactivity eviction (#51343) interrupts the step, and the session goes idle as "Step interrupted". The model never learns what went wrong.

An agent hit this by trying to use execute to poll a review status between checks, with await new Promise(r => r) as a "sleep". The step hung from 15:01:42 to 16:01:59 UTC and ended on the same second that location services evicted was logged for its directory.

Plugins

Local plugins only (session messaging and session PR badges). Not involved.

OpenCode version

v2 dev build 202610080821. The code path is unchanged on current v2.

Steps to reproduce

  1. Ask the agent to run this with execute: await new Promise(r => r); return 1
  2. The tool call stays running and never returns.
  3. After 60 minutes the step ends as "Step interrupted".

Screenshot and/or share link

N/A

Operating System

Debian 13 (Linux 6.12), x86_64

Terminal

Web app and opencode run

Activity

  1. added a commit that references this issue on Oct 10, 2026
    cf2deba
  2. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report. I reproduced this with opencode-drive and the simulated model:

    1. The model calls execute with return 1. It returns at once (control).
    2. The model then calls execute with await new Promise(r => r)\nreturn 1.
    3. About 95 s later the execute row is still spinning, with no timeout or diagnostic. The only way out is interrupting the step.

    This happens on @opencode/cli 2.0.26 (your dev build isn't published, so I used the latest release) and still on the latest dev (7b3d4ce), run from source. The screenshots below show the step still running on each. Exporting a video failed on my side, so no recording is attached. The script below reproduces it.

    In the code, createCodeMode passes no limits, so the interpreter's timeout never applies. The interpreter also doesn't notice that nothing is left that could settle the awaited promise.

    Related: #54220 (no timeout for execute or plugin tool calls in general), PR #54299 (a shared tool timeout), and #51343 (the 60-minute inactivity eviction that eventually ends the step).

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #54340: a Code Mode `execute` program that awaits a promise nothing
    // can settle never finishes; there is no deadline or "deadlock" diagnostic.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const WAIT_SECONDS = Number(process.env.WAIT_SECONDS ?? 90)
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
      },
      ({ llm, ui }) =>
        Effect.gen(function* () {
          // Control: a program that settles normally returns at once.
          yield* llm.queue(
            Llm.toolCall({ index: 0, id: "call_ok", name: "execute", input: { code: "return 1" } }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(Llm.text("Control execute returned."))
          yield* ui.submit("Run return 1 with execute")
          yield* ui.waitFor("Control execute returned.")
          yield* ui.screenshot("control-done")
    
          // The report: awaiting a promise whose resolver is never called.
          yield* llm.queue(
            Llm.toolCall({
              index: 0,
              id: "call_hang",
              name: "execute",
              input: { code: "await new Promise(r => r)\nreturn 1" },
            }),
            Llm.finish("tool-calls"),
          )
          yield* ui.submit("Now run the never-settling promise with execute")
          yield* Effect.sleep("5 seconds")
          yield* ui.screenshot("hang-t5")
          yield* Effect.sleep(`${WAIT_SECONDS} seconds`)
          yield* ui.screenshot(`hang-t${WAIT_SECONDS + 5}`)
          const frame: any = yield* ui.capture()
          yield* Effect.log(
            "\n" + frame.lines.map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n"),
          )
          // Only way out is interrupting the whole step.
          yield* ui.press("escape")
          yield* ui.press("escape")
          yield* Effect.sleep("3 seconds")
          yield* ui.screenshot("after-interrupt")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions