Skip to content

Dual-protocol client: OpenCode V1 and V2 wire APIs - #494

Open
jensenojs wants to merge 87 commits into
sudo-tee:mainfrom
jensenojs:wip-v2
Open

jensenojs wants to merge 87 commits into
sudo-tee:mainfrom
jensenojs:wip-v2

Conversation

@jensenojs

Copy link
Copy Markdown
Collaborator

Summary

Reworks the client to speak both OpenCode wire protocols (V1 1.18.x and V2 2.0.x) through a single connection/observation architecture.

What changed

  • New protocols/{v1,v2} modules own native endpoints and event interpretation per protocol; ui/ consumes one read-only per-session Observation instead of reducing protocol events.
  • Connection selection: authenticated health probe picks the protocol once per connection; reconnect on identity change, no per-request branching.
  • V2 uses the native shared service via CLI discovery (service status / get password); V1 keeps local spawn, explicit URLs, and port coordination.
  • Removed api_client, event_manager, session, renderer/events, event_scope, session_scope.
  • Topbar cost/context stats now derive from observation usage (V2 session.usage.updated / snapshot first, latest entry fallback for V1).
  • run_tests.sh now names the spec file when a test file dies at load time (e.g. requiring a removed module), instead of a bare non-zero exit.

Contract

The published OpenAPI spec and the running 2.0.x server disagree at several endpoints (/api/project/current, rename via POST, prompt command field, fork body shape). This implementation follows the running server; fixtures under tests/data/v2/ are live captures from a real 2.0.1 server. Known future break: upstream dev renames permission.asked / form.* events.

Details in docs/drafts/v2-migration-draft.md (temporary draft intended to be folded into docs later).

Testing

  • ./run_tests.sh green (minimal, unit, replay).
  • Per-protocol contract suites: protocol_{v1,v2}_{operations,observation}*_spec.lua.
  • Live dual-client check done against a real 2.0.3 shared service (native discovery, auth, event stream, prompt admission, both TUI↔nvim visibility).

@jensenojs
jensenojs force-pushed the wip-v2 branch 3 times, most recently from 88f78f1 to bfd06c4 Compare September 16, 2026 00:36
* refactor: replace api_client/event_manager/session with per-protocol
  operations and Observation modules under protocols/{v1,v2}

* feat: auth health probe selects the protocol once per connection;
  identity change forces reconnect, no per-request branching

* feat: connect to the native V2 shared service via CLI discovery;
  V1 keeps local spawn, explicit URLs, and port coordination

* fix(renderer): rebuild topbar stats bridge from observation usage
  (session.usage.updated and snapshot first, latest entry fallback)

* fix(v2): handle session.usage.updated; drop dead branches for
  server-absent session.tool.error and file.watcher.updated

* test: per-protocol contract suites and live-captured V2 fixtures
  from a real 2.0.1 server; counterexamples for cross-session event
  pollution and malformed usage payloads

* docs: add migration draft under docs/drafts; update bidirectional-sync
  recipe for the native V2 service path
@jensenojs

Copy link
Copy Markdown
Collaborator Author

This might be my last chance to clean up the code; I really wish I had enough time...

jensenojs and others added 2 commits September 16, 2026 19:31
gg and scrolling up stopped at the first protocol page in long sessions.
The renderer now pulls older pages through load_older /
load_complete_history and grows its window past the merge, with a single
view-anchor primitive.

- topology: regroup session_tab modules and lru_cache
- draft: reorganize as an architecture baseline
@sudo-tee

Copy link
Copy Markdown
Owner

I started to play with it.

I just fixed a bug in v1 where the panel is in perpetual loading.

I will try to fix the issue I see as they come.

This is a significant change from the prior architecture. I would have liked a simpler and iterative approach. There is a lot of issues at the moment so It's hard to assess if this is a good base or not. I will try to fix some issues and see.

task.tool_action_line reverse-required the formatter registry to look up
child-tool summaries, forming a two-module cycle. The dispatch site
(format_tool) owns the registry, so it now passes it down as an argument.
port_mapping required opencode_server only for its static kill_pid,
forming a two-module cycle. The function is process tooling, not server
identity, so it now lives in util; behavior is unchanged (one level of
children, then the parent).
A flat one-level walk leaked grandchildren as orphans. Real trees are
deeper than one level: the running server spawns MCP/tool processes that
spawn children of their own (measured: serve -> node MCP -> node worker).
Depth-first kill order: grandchildren, children, then the parent.
The no_entry_to_infra comment already acknowledged that :checkhealth
calls infrastructure directly — diagnostics must drive the real
connection lifecycle to verify what they report. Rules can now declare
allowed module pairs, so the exception is machine-checked instead of
living only in a comment. The health -> server_job edge is the first.
The kill_pid move left a duplicated stub block in port_mapping_spec,
making the file unloadable; concurrent stderr replay hid the syntax
error behind '0 failing tests'. The runner now also reports Lua syntax
errors ('...file.lua:NN: ... near ...') as load errors and treats
Scheduling headers as file windows, so a spec that never loads fails
the run instead of passing silently.
@jensenojs

Copy link
Copy Markdown
Collaborator Author

I understand

I started to play with it.

I just fixed a bug in v1 where the panel is in perpetual loading.

I will try to fix the issue I see as they come.

This is a significant change from the prior architecture. I would have liked a simpler and iterative approach. There is a lot of issues at the moment so It's hard to assess if this is a good base or not. I will try to fix some issues and see.

@sudo-tee

Copy link
Copy Markdown
Owner

It's not bad or anything, don't get me wrong.

@jensenojs

Copy link
Copy Markdown
Collaborator Author

I completely understand; even my testing on v2 was insufficient. I just urgently need a version that works on v2

besides, having a flawed implementation to critique is also very good.

…cheduler delay

The resize path schedules its work from the TextChanged autocmd, so the
height settles only when the event loop flushes the callback. Under the
concurrent runner another spec can hold the loop past the 100ms window,
and vim.wait times out with false, -1. Widen the window and surface the
observed height when it still fails.
Comment thread lua/opencode/protocols/v2/operations.lua
jensenojs and others added 25 commits September 21, 2026 08:43
Servers record mention offsets when the mention is inserted; edits to the
surrounding text leave them pointing at the wrong range. Failing the whole
snapshot over one misplaced mention made such sessions impossible to open.
Degrade to a mention-less attachment and log a warning instead.
prompt_body emptied body.files before the user file loop appended into it,
so a submission with file attachments but no editor context crashed on a
nil index. Move the empty check to the end of prompt_body; the wire shape
is unchanged (no files field when there is nothing to attach).

Reported and independently patched by bivashy in PR sudo-tee#494.

Co-authored-by: bivashy <me@bivashy.dev>
Extract protocol health probing into per-protocol connection adapters and
drop the version pin on V1 health checks, requiring only a healthy status.
Own the legacy serve launcher under protocols.v1.server, persist generated
passwords to an owner-only per-port state file, and reuse a configured
fixed-port server before spawning a new one. Probe retries absorb transient
startup transport errors.
The model catalog have the same modelID for different versions of the same model, so we should use the unique id instead.
Add apply_tool_metadata to normalize server-provided file diff metadata
for patch/apply_patch tools into result.changes. Wire it into observation
ingest for tool input and session.tool.success/failed events.

Add patch formatter supporting both part.changes and patchText-based
patches, unnumbered update hunks, and add-file diffs with numbered gutter
highlights. Register shell under the bash formatter to render V2 shell
tools with their native tool name.

Include unit tests for shell/patch rendering and metadata-backed line
numbers.
Add apply_tool_input to map read/edit/write file paths and skill names
from raw tool inputs into normalized formatter fields, and wire it into
both snapshot normalization and observation ingestion.
Make close() preserve hidden output buffers when persist_state is
enabled, matching toggle() and hide() behavior, instead of always
fully tearing down the UI. Update README to document the new close
semantics and add unit tests covering buffer preservation across
close/reopen.
The permission reply body was sent as the raw answer argument. Wrap it
in a table with decision and message fields to match the expected API
payload shape, and assert the serialized body in the unit spec.
Capture file path for pasted images used its full temp path, so @mentions
in prompts never matched and the image was silently dropped. Fall back to
the basename for pasted_image_* files so mentions resolve correctly.

Adds unit test covering basename mention offsets for pasted images.
Replace util.is_temp_path check with a filename match for pasted_image_
prefix in chat_context capture_file, and simplify the related test to
use /tmp directly.
Move revert_message/unrevert_messages onto the Observation layer so
successful actions commit authoritative session state and publish
session-change events before returning.

- v1 and v2 observations now expose revert_message/unrevert_messages via
  _start_state_action, updating session.revert inline
- session handler undo/redo call the Observation instead of reaching into
  connection.operations
- v1 normalize surfaces session.revert for the renderer
- renderer builds the undo summary message from session.revert and hides
  reverted entries on both incremental and full renders
- add unit coverage for handler routing, protocol state commits, and
  renderer reconcile on revert/redo
Break the monolithic V2 observation module into messages, resources,
events, actions, and a shared boundary module, and document the
protocol contract in types.lua. Keep event routing and submission
semantics unchanged; hang validation annotations at the HTTP boundary
instead of duplicating checks in operations.
- add check_types.sh wrapper that resolves VIMRUNTIME for emmylua_check
- configure .emmyrc.json vim globals and ignore dirs
- document emmylua_ls workflow and contracts in AGENTS.md
- fix lint issues: missing icons require, stale type annotations, doc comment cleanup
Split focus subscription from context-related subscriptions so the
debounced reload only fires when losing focus, not when focus returns
to the opencode window. Move open-action resolution before the focus
check so the action is always resolved.
…ession

This will also ensure that the current vim selection is forwarded to the
new one
@sudo-tee
sudo-tee marked this pull request as ready for review September 22, 2026 17:51
@sudo-tee

Copy link
Copy Markdown
Owner

@jensenojs

I think I ran out of bugs or thinks to fix/simplify. It seems to work pretty well for me on both v1 and v2 and the code seems to be well seperated after some polishing

There ia a conflict with main I can't fix since I cannot force push on your branch.

Do you have any remaining issues with this branch ?

Before merging I will create a v1 branch from main so people cans still refers to it if needed.

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.

3 participants