Code review consensus from Tobi+Sandi: the empty-block call
'Opencode::Instrumentation.instrument(name, payload) { }' at fire-
and-forget call sites in opencode-rails is API smell. Tobi: 'two
named verbs are clearer than one verb with a vestigial block.'
Sandi: 'a method with a block parameter that's optional but expected
empty in some call sites is doing two things.'
Two emission shapes now:
.instrument(name, payload) { ... } # block; duration measured
.notify(name, payload) # fire-and-forget; no block
Both flow through the same adapter. The adapter still always
receives a block argument (some adapters key on it, e.g. AS::
Notifications.instrument requires a block) — .notify passes an
empty {}. Adapter return value is ignored for .notify (it returns
nil); .instrument continues to pass through the block's return.
Three new tests in smoke_test.rb:
- no-op when no adapter set
- forwards to adapter + verifies block presence + verifies that
.notify returns nil (not the adapter's return)
- works without a block at the call site
Also: switched gemspec metadata URLs from Gitea to GitHub. The gem
will eventually publish from github.com/ajaynomics/opencode-ruby —
the metadata now reflects that. (No actual GitHub remote push yet;
that's the user's manual step.)
15 tests pass, 32 assertions, 0 failures.
3.3 KiB
3.3 KiB
Changelog
0.0.1.alpha2 — 2026-05-20
Added
Opencode::Instrumentation.notify(name, payload)— fire-and-forget emission for point-in-time events that don't need duration measurement (apply_patch.artifacts_dropped, session.recreated, etc.). Adapter receives an empty block so AS::Notifications-shaped sinks see a zero-duration event. Complements the existing block-form.instrument(name, payload) { ... }.
Why
The block-form .instrument(name, payload) { } with an empty block was
awkward at fire-and-forget call sites in opencode-rails. Two named
verbs (instrument for wrap-a-block, notify for fire-and-forget)
match the host-side mental model and read better at the call site.
0.0.1.alpha1 — Unreleased
First public alpha. HTTP + SSE client for OpenCode REST API.
What's in
Opencode::Client— Net::HTTP-based HTTP client with SSE streaming + automatic reconnection.#create_session(title:, permissions:),#get_messages(session_id),#list_sessions,#delete_session(id),#abort_session(id).#send_message(session_id, text, model:, ...)— synchronous send-and-poll.#send_message_async(session_id, text, ...)— async send.#stream(session_id, text, ...) { |part| ... } → Opencode::Reply::Result— the headline. Block-form streaming with internal Reply accumulation and final-exchange merge.#stream_events(session_id:, ...) { |event| ... }— lower-level SSE event firehose for power users.#reply_question(request_id:, answers:)/#reply_permission(request_id:, reply:)— answer interactive prompts.
Opencode::Reply— live state machine accumulating SSE events into the assistant's reply. Documented observer protocol (Opencode::ReplyObserver).Opencode::Reply::Result— typed Struct value object returned byClient#streamandReply#result. Fields::parts_json,:full_text,:reasoning_text,:tool_parts.Opencode::Instrumentation— pluggable adapter (default no-op). Plug inActiveSupport::Notifications, OpenTelemetry, stdout, etc.Opencode::ResponseParser,Opencode::ToolPart,Opencode::PartSource,Opencode::Todo— wire-format helpers used byReplyand reusable by callers building their own SSE handling.Opencode::Prompts— per-Reply registry of pending question/permission prompts (used byReplyinternally; exposed for callers that need to peek).Opencode::Tracer— callable that prefixes event names before forwarding to a host emitter.- Error hierarchy:
Opencode::Errorand seven subclasses (ConnectionError,TimeoutError,SessionNotFoundError,StaleSessionError,IdleStreamError,ServerError,BadRequestError).
What's out
- ActiveRecord-backed session lifecycle,
acts_as_opencode_session, generators — deferred toopencode-railsif external demand materializes. Seeexamples/conversation_recipe.rbfor the canonical Rails wiring pattern. - Multi-tenant per-user Docker container orchestration — application glue, not a gem's concern.
Compatibility
- Ruby ≥ 3.2
- OpenCode server ≥ 1.15 (tested against the message bus schema in
packages/opencode/src/session/message-v2.ts) - Runtime dependency:
activesupport (>= 6.1)forblank?/present?/presence/truncate/duplicable?/megabytes. ActiveSupport is not Rails — it's a standalone helpers gem.