Skip to content

fix(codemode): fail a program that stalls instead of holding the turn - #54342

Open
laurisvr wants to merge 1 commit into
anomalyco:devfrom
laurisvr:codemode-stall
Open

laurisvr wants to merge 1 commit into
anomalyco:devfrom
laurisvr:codemode-stall

Conversation

@laurisvr

@laurisvr laurisvr commented Oct 10, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #54340

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Code Mode has no timers, so a promise settles only through a tool or extension call, or through the program's own code. A program like await new Promise(r => r) therefore waits forever, and because execute has no time limit, the session holds its turn until inactivity eviction interrupts it an hour later.

The interpreter now counts its own steps and the host calls in flight, including their hooks. A new optional stallMs limit fails the program with a Stalled diagnostic once a whole window passes with no step taken and no call running. In that state nothing can ever settle the promise, so failing is always right. Slow tool calls and busy loops never trip it. Without stallMs, behavior is unchanged.

The execute tool sets stallMs to 30 s and tells the model not to use execute to wait or poll. I chose this over a wall-clock timeout because a timeout would also cut off legitimate long tool calls.

The hot-path cost is one integer increment per statement or expression. I measured no difference on a 400k-iteration loop.

How did you verify your code works?

  • New tests in packages/codemode/test/codemode.test.ts cover the never-settling forms, logs and calls kept before the stall, slow calls and hooks, busy loops, un-awaited promises at return, and how the stall interacts with timeoutMs. A new TestClock test in packages/core/test/tool-execute.test.ts fails without the fix.
  • Ran the exact hanging program through a dev server: it returned Execution stalled… after 30 s.
  • bun run lint passes, typecheck passes for 36/36 packages on dev, and codemode tests are 1408 pass.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #54220, #51223

Public API: adds the optional stallMs execution limit and the Stalled diagnostic kind to @opencode/codemode (backwards compatible)

A Code Mode program has no timers, so a promise settles only through a
tool or extension call or the program's own steps. A program awaiting a
promise that nothing can settle, such as `await new Promise(r => r)`,
waited forever, and with no execute timeout the session held its turn
until inactivity eviction interrupted it an hour later.

The interpreter now counts its steps and the host calls in flight,
including their hooks. A new optional `stallMs` limit fails the program
with a `Stalled` diagnostic once a full window passes with no step and no
call running. OpenCode's execute tool sets it to 30 seconds and tells the
model not to use execute to wait or poll.

Closes anomalyco#54340
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I reproduced the hang on the base branch: in the TUI with the simulated model, an execute call with await new Promise(r => r); return 1 was still running after 50s. With this PR it fails after about 30s with the Execution stalled… message, and the model gets the error and the turn ends. The codemode and core tool-execute tests pass, and so do both packages' typechecks. The new TestClock test times out when run against the base code, so it really does catch the bug.

I checked how stalls are detected: every tool call (including search) and every extension call is counted as running while in flight, along with its before/after hooks. Slow calls and slow permission hooks therefore can't be mistaken for a stall. After return, the stall check stops and cleanup is still bounded only by timeoutMs.

Public API: @opencode/codemode gains an optional ExecutionLimits.stallMs, a new Stalled value in DiagnosticKind, and a new stallMs field on ResolvedExecutionLimits. All are additions and backwards compatible; only code that matches every DiagnosticKind value exhaustively would need to handle Stalled. Config, CLI, HTTP API and SDK are unchanged.

Related: #54220, #51223

Before

After

This branch has not been deployed

No deployments
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.

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

1 participant