Implement the real OpenCode event contract; reject headless questions (#28)#36
Merged
TheRealDinghyDog merged 1 commit intoJul 10, 2026
Merged
Conversation
…#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.updatedcarries only message metadata inproperties.info— the client looked for parts arrays on it that never exist.message.part.updated(properties.part) andmessage.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/messageresponse dropped and HTTP recovery was unavailable (the review's targeted reproduction).turnIdcould pick up theevt_…envelope id instead of the assistant message id.session.createdwithproperties.info.parentID, which the client didn't read.question.asked/question.v2.askedhad 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):message.updatedis metadata-only: assistant message id (fixesturnId) andinfo.structuredfor json_schema output.session.created/session.updatedregister children fromproperties.info; subagent text is labeled in progress logs and excluded from the final message.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
/docschema"):tests/opencode-event-contract.jsonis pinned from a real 1.17.15/doc: envelope requirements, per-eventpropertiesrequired/allowed keys, and required keys forSession/AssistantMessage/UserMessage/TextPart/ReasoningPart/ToolPart.{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
fetch failedon long turns + no result recovery #2/Transport: /message POST held open for the whole turn — long turns fail to return (follow-up to #2) #12/Transport hardening (post-#12): SSE-drop loses a valid turn; recovery has no timeout #15 transport fail-mode regressions re-verified under the real shapes.npm test: 89 passed, 0 failed, 0 skipped.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