Skip to content

fix(cli): emit user prompt as a user event in run --format json#29998

Open
Kurry wants to merge 1 commit into
anomalyco:devfrom
Kurry:fix/run-json-omits-user-message
Open

fix(cli): emit user prompt as a user event in run --format json#29998
Kurry wants to merge 1 commit into
anomalyco:devfrom
Kurry:fix/run-json-omits-user-message

Conversation

@Kurry
Copy link
Copy Markdown

@Kurry Kurry commented May 30, 2026

Issue for this PR

Closes #29997

Type of change

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

What does this PR do?

opencode run --format json never emits the user's prompt. The stream begins at step_start, so anything reconstructing a transcript from it (eval harnesses, loggers, trajectory exporters) loses the user turn.

The prompt is recorded — it's just dropped from the stream. Take one session on dev: run --format json omits the prompt, but opencode export of that same session contains it.

opencode run --format json -- say hello on dev (the real format is one compact JSON object per line; pretty-printed here, IDs shortened):

{ "type": "step_start",  "sessionID": "ses_…", "part": { "id": "prt_…", "type": "step-start", "snapshot": "" } }
{ "type": "text",        "sessionID": "ses_…", "part": { "type": "text", "text": "Hello! How can I assist you today?", "time": { "start": 1780155478703, "end": 1780155478922 } } }
{ "type": "step_finish", "sessionID": "ses_…", "part": { "reason": "stop", "type": "step-finish", "tokens": { "total": 14759, "input": 14748, "output": 11 }, "cost": 0.0022188 } }

The first event is the assistant's step_start — the prompt is nowhere in the stream.

opencode export <same session> on dev (excerpt) — the prompt is right there:

{
  "messages": [
    { "info": { "role": "user" },      "parts": [{ "type": "text", "text": "say hello" }] },
    { "info": { "role": "assistant" }, "parts": [{ "type": "text", "text": "Hello! How can I assist you today?" }] }
  ]
}

So the data exists; the --format json stream just never emits it.

Why the stream drops it. The event loop receives a message.part.updated for every part, including the user's prompt, but the text branch only emits once part.time?.end is set:

// src/cli/cmd/run.ts:688
if (part.type === "text" && part.time?.end) {
  if (emit("text", { part })) continue
  ...
}

That gate is deliberate: assistant text streams in as several updates, so it waits for the finished one (the part that has a time.end). But the user's prompt is input, not streamed generation — its text part arrives once with no time field at all, so part.time?.end is undefined, the branch is skipped, and nothing else in the loop handles it. The same guard that dedupes streaming assistant text silently drops the prompt. Logging every event the loop sees confirms it:

part.updated  type=text  messageID=<user>       time=undefined                  <- prompt, dropped by the gate
part.updated  type=text  messageID=<assistant>  time={"start":…}                <- partial, correctly skipped
part.updated  type=text  messageID=<assistant>  time={"start":…,"end":…}        <- emitted

The fix. Emit a user event carrying the same parts passed to session.prompt, just before the assistant turn starts. It reuses the existing emit() helper (a no-op unless --format json), so default output is unchanged, and it's additive — consumers that don't know the user type ignore it.

With the fix, opencode run --format json -- say hello produces the full trajectory below — the new user event leads and the rest is identical to dev:

{
  "type": "user",
  "timestamp": 1780155194554,
  "sessionID": "ses_1867b634affeNm9y7CZN9ojxPl",
  "parts": [{ "type": "text", "text": "say hello" }]
}
{ "type": "step_start",  "sessionID": "ses_…", "part": { "id": "prt_…", "type": "step-start", "snapshot": "" } }
{ "type": "text",        "sessionID": "ses_…", "part": { "type": "text", "text": "Hello! How can I assist you today?", "time": { "start": 1780155478703, "end": 1780155478922 } } }
{ "type": "step_finish", "sessionID": "ses_…", "part": { "reason": "stop", "type": "step-finish", "tokens": { "total": 14759, "input": 14748, "output": 11 }, "cost": 0.0022188 } }

How did you verify your code works?

  • Captured the run-vs-export contrast above on dev (same session): run --format json has no user turn, export does.
  • Added a subprocess regression test in test/cli/run/run-process.test.ts (drives the real CLI with --format json) asserting a user event carries the prompt and precedes the assistant text. It fails on dev (no user event → expect(user).toBeDefined() fails) and passes with the fix.
  • bun test test/cli/run/run-process.test.ts → 5 pass.
  • bun typecheck clean; prettier + oxlint clean on the changed files.

Screenshots / recordings

N/A — not a UI change.

Checklist

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

`opencode run --format json` began the event stream at step_start and
never emitted the prompt itself, so anything rebuilding a transcript from
the stream lost the user turn — it was only recoverable via
`opencode export`. The streamed message.part events only cover the
assistant reply, and a user text part has no time.end, so the existing
text handler skipped it.

Emit the prompt as a `user` event (carrying the same parts passed to
session.prompt) before the assistant turn begins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels May 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

alexgshaw pushed a commit to harbor-framework/harbor that referenced this pull request May 30, 2026
…jectory (#1759)

OpenCode trajectories had no source="user" step: _convert_events_to_trajectory
only emitted agent steps, so the prompt was missing (the docstring even claimed
a user step was synthesised, but the code never added one).

OpenCode's `run --format=json` stream omits the prompt entirely
(anomalyco/opencode#29997); it is only recoverable via `opencode export`.
Capture the rendered instruction in run() and prepend a source="user" step,
preferring OpenCode's own `user` event when present (forward-compatible with
anomalyco/opencode#29998) and falling back to the instruction otherwise.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

opencode run --format json never emits the user prompt message

1 participant