Skip to content

fix(cli): emit final step events when run settles before they arrive - #53208

Closed
nkootstra wants to merge 8 commits into
anomalyco:devfrom
nkootstra:run-json-step-finish
Closed

nkootstra wants to merge 8 commits into
anomalyco:devfrom
nkootstra:run-json-step-finish

Conversation

@nkootstra

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #53207

Type of change

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

What does this PR do?

opencode run settles on session.wait. After that it ignores queued live events and fills in text, reasoning and tool parts from the saved messages in reconcile(). Step boundaries weren't filled in, so when idle arrived before the final session.step.ended, the final step_finish (with tokens and cost) was never printed. Sometimes its step_start was lost too.

The fix tracks which steps were already printed from live events. reconcile() then prints any missing step_start/step_finish from the saved assistant message: finish, snapshot, cost and tokens. Failed steps (saved with an error) still only print the error, as on the live path.

How did you verify your code works?

  • Added tests:
    • idle wins before a step's events arrive (failed before this change)
    • no duplicate step events when they arrived live
    • a failed final step gets no step_finish
  • Updated the existing reconcile test, which asserted the old output.
  • bun test test/run and bun run check pass.
  • Live with OpenCode Go (deepseek-v4-flash, glm-5.3-flash): 0/9 runs had the final step_finish before this change, 20/20 after, with no duplicates.

Screenshots / recordings

N/A, CLI output only.

Checklist

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

`opencode run --format json` settles on `session.wait` and then ignores
queued live events, reconciling text, reasoning, and tools from the
projected messages. Step boundaries were not reconciled, so when idle
won the race the final `step_finish` (with tokens and cost) and
sometimes its `step_start` were never printed.

Reconcile missing `step_start` and `step_finish` from the projected
assistant message, skipping steps already emitted live and failed steps.
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

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

@thdxr
thdxr changed the base branch from v2 to dev October 10, 2026 19:32
@oldantique

Copy link
Copy Markdown

Tested against real runs: the PR head vs. the same tree with only the noninteractive.ts change reverted, --standalone --format json, deepseek/deepseek-v4-flash, fresh git repo, two plain replies and two runs with a tool call per side.

  • without the fix: 0/4 runs printed the final step_finish (streams ended on text)
  • with the fix: 4/4 did, reason stop with tokens. In the tool-call runs the earlier steps were emitted live and only the last step_finish came from reconcile, with no duplicates.

Default format output is unchanged and noninteractive.test.ts passes (14/14). Same symptom on the released v2.0.26 (#54661, closed as a duplicate of #53207). LGTM.

@JavierGOrdonnez

Copy link
Copy Markdown

Downstream consumer here — opencode run --format json ending with the last step still open is exactly what makes our task runner mark fully-delivered runs as failed (multica-ai/multica#8892) and then auto-retry them.

Reproduced on v2.0.26 and v2.0.29: stdout carries step_start + the text parts, exit code is 0, no error event, no step_finish.

We're bridging locally with a wrapper that appends one synthetic step_finish (reason stop, zero usage) only on clean EOF with a step still open. That works but reports usage/cost as zero, since the real totals only exist in the event OpenCode drops — so this PR is the fix we actually want. Happy to build this branch against v2.0.26/v2.0.29 and verify the terminal event lands for our runner if that's useful for getting CI green.

@oldantique

Copy link
Copy Markdown

For the zero-usage problem in the meantime: the session itself has the real numbers. opencode session export --standalone <sessionID> (the id is on every event line) returns .info.outcome ("succeeded", "failed" on a provider error, null if the run was killed) plus .info.cost and .info.tokens, so a wrapper can decide success and record usage from that instead of the stream. Checked on 2.0.29. (Without --standalone it starts the background service.)

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: opencode run --format json drops the final step_finish event

3 participants