Skip to content

Implement the real OpenCode event contract; reject headless questions (#28)#36

Merged
TheRealDinghyDog merged 1 commit into
opencode-conversionfrom
fix/issue-28-event-contract
Jul 10, 2026
Merged

Implement the real OpenCode event contract; reject headless questions (#28)#36
TheRealDinghyDog merged 1 commit into
opencode-conversionfrom
fix/issue-28-event-contract

Conversation

@TheRealDinghyDog

Copy link
Copy Markdown
Owner

Fixes #28 (review findings H-03 and M-02).

Problem

The SSE client parsed an invented event shape. Verified against the real 1.17.15 OpenAPI document:

  • message.updated carries only message metadata in properties.info — the client looked for parts arrays on it that never exist.
  • Streamed content arrives exclusively via message.part.updated (properties.part) and message.part.delta, which the client never handled — so no live text, reasoning, or structured-output part was ever captured from the stream, and a valid answer was reported as failed whenever the held-open /message response dropped and HTTP recovery was unavailable (the review's targeted reproduction).
  • turnId could pick up the evt_… envelope id instead of the assistant message id.
  • Child sessions announce themselves via session.created with properties.info.parentID, which the client didn't read.
  • question.asked/question.v2.asked had no handler: a plan-agent question stalled the turn for the full 30-minute ceiling (M-02, reproduced in the review).

The fake test server emitted the same invented shapes, so the suite reinforced the bug.

Fix

Client (opencode.mjs, opencode-server.mjs):

  • Part snapshots accumulate by part id (insertion order preserved); the main-session final message is assembled from the turn's assistant-message text parts. Deltas append to known parts; the next snapshot stays authoritative — so mixed snapshot/delta streams self-correct.
  • message.updated is metadata-only: assistant message id (fixes turnId) and info.structured for json_schema output.
  • session.created/session.updated register children from properties.info; subagent text is labeled in progress logs and excluded from the final message.
  • Questions are rejected immediately via the new rejectQuestion() (POST /question/{requestID}/reject, no body — per the OpenAPI doc) with a diagnostic naming the question; deterministic headless policy per the review's recommendation.

Contract pinning (the review's "generate fixtures from a captured /doc schema"):

  • tests/opencode-event-contract.json is pinned from a real 1.17.15 /doc: envelope requirements, per-event properties required/allowed keys, and required keys for Session/AssistantMessage/UserMessage/TextPart/ReasoningPart/ToolPart.
  • The fixture now emits the real envelope ({id: "evt_…", type, properties}) with contract-complete objects, and a new contract test drives it over raw HTTP, validating every emitted event against the pin — the fixture can no longer drift back to invented shapes.

Tests

Not covered here: version-matrix compatibility testing against a minimum supported OpenCode (tracked as part of #33's CI work); a provider-backed live turn remains on the release checklist.

🤖 Generated with Claude Code

…#28)

The SSE client parsed an invented event shape: it looked for parts on
message.updated and never handled message.part.updated, so against a real
server (1.17.15 OpenAPI verified) no streamed text, reasoning, or
structured-output part ever arrived — a valid answer could be reported as
failed whenever the held-open /message response dropped and HTTP recovery
was unavailable. Question events had no handler at all, so a plan-agent
question stalled the turn for the full 30-minute ceiling.

Client changes:
- message.part.updated: accumulate part snapshots by part id (insertion
  order preserved) and assemble the main-session final message from the
  turn's assistant message text parts.
- message.part.delta: append field deltas to known parts; the next
  snapshot stays authoritative.
- message.updated is metadata-only (properties.info): assistant message
  id (fixes turnId picking up evt_ envelope ids) and json_schema output
  via info.structured.
- session.created/session.updated register child sessions from
  properties.info (id/parentID/title/agent), so subagent output is
  labeled instead of polluting the final message.
- question.asked / question.v2.asked are rejected immediately via
  POST /question/{requestID}/reject with a progress diagnostic — the
  model must proceed autonomously instead of stalling headlessly.

The fake fixture now emits the real envelope ({id: evt_*, type,
properties}) with contract-complete Session/AssistantMessage/Part
objects, and a new contract test drives it over raw HTTP and validates
every emitted event against tests/opencode-event-contract.json, pinned
from a real 1.17.15 /doc — the fixture can no longer drift back to
invented shapes. New regression tests cover question rejection,
delta-only final-message assembly (dropped POST, recovery withheld), and
subagent isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@TheRealDinghyDog
TheRealDinghyDog merged commit ed8ac3d into opencode-conversion Jul 10, 2026
1 check passed
@TheRealDinghyDog
TheRealDinghyDog deleted the fix/issue-28-event-contract branch July 11, 2026 00:55
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.

1 participant