Skip to content

Fix Hermes and OpenCode web session transports - #297

Merged
donovan-yohan merged 2 commits into
nightlyfrom
dy/fix-web-session-adapters
Apr 24, 2026
Merged

Fix Hermes and OpenCode web session transports#297
donovan-yohan merged 2 commits into
nightlyfrom
dy/fix-web-session-adapters

Conversation

@donovan-yohan

@donovan-yohan donovan-yohan commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes web-mode agent session transports for Hermes and OpenCode:

  • Aligns Hermes with the installed gateway API server contract by enabling the local API server with env vars, waiting on /health, and streaming turns through /v1/responses.
  • Replaces OpenCode web-mode stdin/TUI driving with the same server transport its web UI uses: opencode serve, REST session creation, prompt_async with parts, and /global/event SSE updates.
  • Adds focused stubs and e2e coverage for Hermes and OpenCode web sessions sending prompts and receiving streamed chat events.

Verification

  • npm run check
  • npm run build:server && npx vitest run test/opencode-adapter.e2e.test.ts test/hermes-adapter.e2e.test.ts test/hermes-session-api.e2e.test.ts test/web-session-handler.test.ts test/customize-session-dialog.test.ts test/adapter-map-hook-event.test.ts test/opencode-relay.test.ts
  • npm run build (passes with existing Vite chunk/dynamic-import warnings)
  • Push hook: 10 test files, 151 tests passed

Summary by CodeRabbit

  • New Features

    • Integrated OpenAI-compatible API streaming support to protocol adapters.
  • Bug Fixes

    • Enhanced error reporting with improved failure detection and health monitoring.
  • Refactor

    • Modernized protocol adapter implementations with REST + SSE architecture.
  • Tests

    • Added end-to-end tests for adapter functionality and message streaming.

Copilot AI review requested due to automatic review settings April 24, 2026 21:54
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@donovan-yohan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 016e5739-963a-4afc-9a09-b41ab260a2bb

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd9fe1 and f65f9f9.

📒 Files selected for processing (5)
  • server/protocol-adapters/hermes-adapter.ts
  • server/protocol-adapters/opencode-adapter.ts
  • test/fixtures/hermes-gateway-stub.cjs
  • test/fixtures/opencode-serve-stub.cjs
  • test/opencode-adapter.e2e.test.ts
📝 Walkthrough

Walkthrough

The pull request refactors two protocol adapters (Hermes and OpenCode) to transition from event-polling mechanisms to REST+streaming APIs. Hermes now spawns a local API server and consumes OpenAI-compatible Responses streaming, while OpenCode adopts a REST endpoint pattern with SSE event streams. New test fixtures and E2E tests validate both flows.

Changes

Cohort / File(s) Summary
Hermes Adapter
server/protocol-adapters/hermes-adapter.ts
Switched from consuming legacy /events SSE and REST prompt/abort endpoints to spawning local hermes gateway run with API-server config, generating API keys, polling /health, and streaming responses via POST /v1/responses with parsing of SSE event blocks and tool-call emission. interrupt() now aborts the active stream controller; approval responses emit directly rather than via REST.
OpenCode Adapter
server/protocol-adapters/opencode-adapter.ts
Replaced hook/relay-plugin-based BaseHookAdapter with BaseProtocolAdapter using REST + SSE. Spawns OpenCode server with dynamic port allocation, polls /health, creates sessions via POST /session, consumes streamed events from /global/event, sends prompts via POST /session/{id}/prompt_async, and handles tool execution and permission approvals through new handler methods. Maintains backward compatibility with legacy HookEventPayload mapping.
Test Fixtures
test/fixtures/hermes-gateway-stub.cjs, test/fixtures/opencode-serve-stub.cjs
Hermes stub rewritten to mock OpenAI-compatible POST /v1/responses streaming with Responses-specific event names. OpenCode stub newly introduced as standalone HTTP server that responds to health checks, session creation, prompt async calls with streamed deltas, and abort requests on port configured via environment or CLI.
OpenCode E2E Test
test/opencode-adapter.e2e.test.ts
New Vitest E2E test validating the OpenCode adapter end-to-end: spins up stub server, sends a message, verifies session transitions to idle, confirms chat events include text deltas and turn completion, validates response content.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant HermesAdapter
    participant GatewayProcess as Hermes Gateway<br/>(spawned process)
    participant APIServer as API Server<br/>(:PORT)

    Client->>HermesAdapter: initialize()
    HermesAdapter->>GatewayProcess: spawn with API_SERVER_PORT env
    HermesAdapter->>APIServer: poll /health (loop until ready)
    APIServer-->>HermesAdapter: 200 OK

    Client->>HermesAdapter: sendMessage(turnId, content)
    HermesAdapter->>APIServer: POST /v1/responses (stream=true)
    APIServer-->>HermesAdapter: SSE stream (response.created, response.output_text.delta...)
    HermesAdapter->>HermesAdapter: parse event:/data: blocks
    HermesAdapter-->>Client: emit chat:text-delta
    APIServer-->>HermesAdapter: response.output_item.added (tool call)
    HermesAdapter-->>Client: emit chat:tool-call
    APIServer-->>HermesAdapter: response.completed
    HermesAdapter-->>Client: emit chat:turn-completed

    Client->>HermesAdapter: interrupt()
    HermesAdapter->>HermesAdapter: abort active stream controller
Loading
sequenceDiagram
    participant Client
    participant OpenCodeAdapter
    participant ServerProcess as OpenCode Server<br/>(spawned process)
    participant RESTServer as REST Server<br/>(:PORT)

    Client->>OpenCodeAdapter: initialize()
    OpenCodeAdapter->>ServerProcess: spawn with PORT env
    OpenCodeAdapter->>RESTServer: poll /global/health (loop until ready)
    RESTServer-->>OpenCodeAdapter: 200 OK

    OpenCodeAdapter->>RESTServer: POST /session
    RESTServer-->>OpenCodeAdapter: {ses_id: '...'}
    OpenCodeAdapter->>RESTServer: subscribe to /global/event (SSE)
    RESTServer-->>OpenCodeAdapter: server.connected event

    Client->>OpenCodeAdapter: sendMessage(turnId, content)
    OpenCodeAdapter->>RESTServer: POST /session/{id}/prompt_async
    RESTServer-->>OpenCodeAdapter: 204 No Content
    RESTServer-->>OpenCodeAdapter: SSE session.status (active → idle)
    RESTServer-->>OpenCodeAdapter: SSE message.part.updated (text delta)
    OpenCodeAdapter-->>Client: emit chat:text-delta
    OpenCodeAdapter-->>Client: emit chat:turn-completed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes introduce significant architectural shifts across two adapters with heterogeneous patterns (Hermes streaming vs. OpenCode REST+SSE), require careful validation of new initialization flows, event parsing logic, and error handling paths, plus new test fixtures and E2E coverage that each demand separate reasoning.

Poem

🐰 Gateway calls now stream with grace,
New APIs take the Hermes' place!
OpenCode hops through REST and flow,
Where health checks bloom and SSE grow.
The adapter herd bounds swift and lean,
Code refactored, tests pristine! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix Hermes and OpenCode web session transports' directly summarizes the main change: fixing the transport mechanisms for both Hermes and OpenCode web session adapters. It is concise and specific.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dy/fix-web-session-adapters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the web-mode transport implementations for the Hermes and OpenCode protocol adapters to match their respective HTTP/SSE server contracts, and adds test stubs + e2e coverage to validate prompt sending and streamed event ingestion.

Changes:

  • Reworks OpenCode web sessions to spawn opencode serve, create REST sessions, send prompts via prompt_async with parts, and consume /global/event SSE.
  • Updates Hermes web sessions to enable the local API server via env vars, wait for /health, and stream turns through /v1/responses SSE.
  • Adds adapter-focused e2e tests plus lightweight HTTP stubs for OpenCode serve and the Hermes API server.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/protocol-adapters/opencode-adapter.ts Replaces legacy hook/TUI driving with OpenCode’s HTTP + SSE server transport.
server/protocol-adapters/hermes-adapter.ts Switches Hermes to API-server + OpenAI-compatible Responses streaming transport.
test/opencode-adapter.e2e.test.ts E2E test verifying OpenCode prompt + streamed chat events through the new transport.
test/fixtures/opencode-serve-stub.cjs Stub server implementing the minimal OpenCode serve REST/SSE surface needed by the adapter.
test/fixtures/hermes-gateway-stub.cjs Updates the Hermes stub to stream OpenAI Responses-style SSE events.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/opencode-adapter.ts
Comment thread test/fixtures/hermes-gateway-stub.cjs Outdated
const port = parseInt(process.argv[2], 10);
const port = parseInt(process.env.API_SERVER_PORT || process.argv[2], 10);
if (!port || isNaN(port)) {
console.error('Usage: node hermes-gateway-stub.js <port>');

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage comment says to run with API_SERVER_PORT, but the actual error message still prints the old positional-arg usage. Update the message to match the supported invocation (env var and/or argv fallback) so failures are easier to diagnose.

Suggested change
console.error('Usage: node hermes-gateway-stub.js <port>');
console.error(
'Usage: API_SERVER_PORT=1234 node hermes-gateway-stub.js (or: node hermes-gateway-stub.js <port>)'
);

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/protocol-adapters/opencode-adapter.ts (1)

519-542: ⚠️ Potential issue | 🟡 Minor

session.status === 'error' leaves the current turn open.

The 'active' and 'error' branches return early without clearing _currentTurnId or emitting chat:turn-completed. As a result, after a session-level error the turn is effectively stuck — subsequent SSE events (message.part.updated, tool.execute.*, etc.) will continue to be attributed to the already-failed turn, and the UI will never see a turn completion for it. handleSessionError does this correctly; handleSessionStatus for 'error' should do the same.

🐛 Proposed fix
     if (status === 'error') {
+      if (this._currentTurnId) {
+        this.fire({
+          type: 'chat:turn-completed',
+          turnId: this._currentTurnId,
+          reason: 'failed',
+          durationMs: 0,
+          toolCallCount: 0,
+          messageCount: 0,
+        });
+        this._currentTurnId = null;
+      }
       this.fire({ type: 'chat:session-status', status: 'error' });
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/protocol-adapters/opencode-adapter.ts` around lines 519 - 542,
handleSessionStatus currently returns early on status === 'error' leaving
_currentTurnId intact and not emitting a chat:turn-completed; update
handleSessionStatus to mirror handleSessionError by, when status === 'error',
checking if this._currentTurnId is set, firing a chat:turn-completed event
(include the same fields used by handleSessionError: turnId:
this._currentTurnId, reason: 'failed' or 'error' consistent with
handleSessionError, durationMs, toolCallCount, messageCount), clearing
this._currentTurnId, and then firing the session-status/error event so the turn
is closed and subsequent events are not attributed to the stale turn.
🧹 Nitpick comments (6)
server/protocol-adapters/hermes-adapter.ts (1)

266-274: Dead code in respondToInput.

firstAnswer is extracted but never consumed — the function is a no-op regardless of its value. Drop the unused logic or replace with an explicit unsupported signal so future readers aren't misled.

🧹 Proposed cleanup
   async respondToInput(
     _requestId: string,
-    answers: Record<string, string[]>
+    _answers: Record<string, string[]>
   ): Promise<void> {
-    const firstAnswer = Object.values(answers)[0]?.[0];
-    if (!firstAnswer) return;
     // Hermes gateway does not currently support structured input questions
     // via REST; this is a no-op.
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/protocol-adapters/hermes-adapter.ts` around lines 266 - 274, The
respondToInput function extracts firstAnswer but never uses it, leaving
misleading dead code; remove the unused extraction (firstAnswer) and either
leave the function as an explicit no-op with a clarifying comment or replace it
with a clear unsupported operation signal (e.g., throw or return a specific "not
supported" response) so callers/readers of respondToInput know input handling is
intentionally unsupported by the Hermes gateway.
test/fixtures/hermes-gateway-stub.cjs (1)

44-56: Guard JSON.parse to avoid crashing the stub on malformed bodies.

A malformed request body currently throws out of the end handler and kills the stub process, which can mask the real adapter failure under test. Cheap to wrap:

🛡️ Proposed fix
     req.on('end', () => {
-      const data = JSON.parse(body);
+      let data;
+      try {
+        data = JSON.parse(body);
+      } catch (err) {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ error: String(err) }));
+        return;
+      }
       res.writeHead(200, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/hermes-gateway-stub.cjs` around lines 44 - 56, Wrap the
JSON.parse(body) inside a try/catch in the req.on('end') handler to prevent the
stub from crashing on malformed bodies; on parse failure log the error and
return an HTTP error response (e.g., 400) instead of calling emitTurn,
referencing the req.on('end') callback, the local variable body, and the
emitTurn(res, data.input) call so you only call emitTurn when parsing succeeds.
test/opencode-adapter.e2e.test.ts (2)

17-19: Prefer a typed vi.fn over as unknown as.

The double-cast silently bypasses type checking. Vitest supports a generic form that gives you the same mock with proper typing.

♻️ Proposed fix
-  const onBackendStateChanged = vi.fn() as unknown as (
-    session: Session
-  ) => void;
+  const onBackendStateChanged = vi.fn<(session: Session) => void>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/opencode-adapter.e2e.test.ts` around lines 17 - 19, The test creates
onBackendStateChanged using a double-cast (vi.fn() as unknown as (session:
Session) => void) which bypasses type checking; replace this with a properly
typed mock by using Vitest’s generic form vi.fn<ReturnType,
ArgTypes>()—specifically change the creation of onBackendStateChanged to use
vi.fn<void, [Session]> (or vi.fn<(session: Session) => void>) so the mock is
correctly typed without the unsafe as unknown as cast.

49-53: Consider an explicit timeout on vi.waitFor for CI robustness.

The stub paces events up to ~100ms but this test also spawns a Node child process and waits for /global/health — on slow CI the combined latency can flirt with vitest's default waitFor timeout. Setting an explicit, generous timeout removes a future flake class without changing normal-run behavior.

♻️ Proposed fix
-  await vi.waitFor(() => {
+  await vi.waitFor(() => {
     expect(session.agentState).toBe('idle');
     expect(events.some((e) => e.type === 'chat:text-delta')).toBe(true);
     expect(events.some((e) => e.type === 'chat:turn-completed')).toBe(true);
-  });
+  }, { timeout: 5000, interval: 25 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/opencode-adapter.e2e.test.ts` around lines 49 - 53, The test uses
vi.waitFor without an explicit timeout which can flake on slow CI; update the
vi.waitFor call (the block asserting session.agentState and events) to include a
generous explicit timeout option (e.g., 3000–10000 ms) so the waitFor for
session.agentState and the chat events has more time on CI; keep the same
assertions (session.agentState, events.some for 'chat:text-delta' and
'chat:turn-completed') but pass the timeout option to vi.waitFor to ensure
robustness.
test/fixtures/opencode-serve-stub.cjs (1)

65-76: Wrap JSON.parse so malformed bodies return 400 instead of crashing the stub.

If the adapter ever sends a malformed body, JSON.parse(body) throws synchronously from the 'end' callback and the entire stub process dies with an unhandled exception, producing an opaque E2E failure instead of a clear assertion against the adapter. Also note that payload is untyped — if a client sends null or a non-object, payload.parts throws again.

♻️ Proposed fix
     drain(req, (body) => {
-      const payload = JSON.parse(body);
-      if (!Array.isArray(payload.parts)) {
+      let payload;
+      try {
+        payload = JSON.parse(body);
+      } catch {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ error: 'invalid JSON body' }));
+        return;
+      }
+      if (!payload || !Array.isArray(payload.parts)) {
         res.writeHead(400, { 'Content-Type': 'application/json' });
         res.end(JSON.stringify({ error: 'parts is required' }));
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/opencode-serve-stub.cjs` around lines 65 - 76, Wrap the
synchronous JSON.parse inside the drain callback in a try/catch and return a 400
JSON response on parse errors; after parsing also defensively check that the
parsed value is a non-null object and that payload.parts is an array (use
Array.isArray(payload.parts)), returning the same 400 JSON error if those
validations fail, then only proceed to writeHead(204), res.end(), and call
emitTurn(); update the block around drain(req, (body) => { ... }) accordingly to
avoid throwing on malformed bodies or non-object payloads.
server/protocol-adapters/opencode-adapter.ts (1)

82-108: getPort + spawn is a TOCTOU race; prefer binding and reading the actual port.

getPort() reports a port that's free now, but there's a window before the spawned opencode serve actually binds it during which another process can claim it — typically manifesting as sporadic CI failures and a cryptic EADDRINUSE traced back to waitForServer. If opencode serve can print its listening port to stdout (or accept --port 0 and report it), parsing that output is more reliable than pre-allocating.

At minimum, consider adding a dedicated error path in waitForServer when the bound port logs indicate EADDRINUSE, so users can distinguish a race from a genuinely slow startup.

opencode serve CLI flags print bound port stdout 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/protocol-adapters/opencode-adapter.ts` around lines 82 - 108, The
current use of getPort() plus spawn creates a TOCTOU race; modify the startup
flow so opencode is allowed to bind the port and the adapter reads the actual
bound port from the child process output instead of preallocating one.
Concretely: when building args in the constructor (see command, defaultArgs,
args) allow passing '--port', '0' (or detect a {{PORT}} placeholder) so the
server selects a free port, then listen to this._process.stdout for the CLI line
that reports the bound port (parse and assign this._apiPort and recompute
this._endpoint) before resolving readiness; as a fallback keep the existing
getPort() behavior. Also update waitForServer to detect an EADDRINUSE error
emitted by the child (or printed to stderr) and surface a distinct, actionable
error so races are distinguishable from slow startups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/protocol-adapters/hermes-adapter.ts`:
- Around line 188-196: _reset _lastResponseId on failure paths: ensure
this._lastResponseId is cleared whenever a response fails or is aborted and
during disconnect/reconnect startup to prevent leaking an invalid
previous_response_id; specifically, clear it in the response.failed handler and
where AbortError is caught, in onDisconnect, and at the start of
connect/reconnect before calling sendMessage or building the request body so
sendMessage and the body construction (which currently uses
this._lastResponseId) never include a stale id from a prior failed/aborted turn.
- Around line 254-264: The Hermes adapter currently only fires a local event in
respondToApproval and never emits chat:approval-request because it doesn't
handle permission hook events; modify the adapter so that when processing
Hermes/Responses API hook events you detect the permission.requested hook and
emit a chat:approval-request with the permission id and metadata (matching other
adapters' behavior), and update respondToApproval to call the backend permission
endpoints (/permission/:id/allow or /permission/:id/deny) based on the decision
(mapping 'allow-always' to allow as needed) and then fire the existing local
chat:approval-response; keep the method name respondToApproval and the
SSE/responses handling code paths so tests/stub endpoints expecting
/permission/:id/(allow|deny) work.

In `@server/protocol-adapters/opencode-adapter.ts`:
- Around line 307-328: In respondToApproval: preserve the three-way mapping
required by OpenCode (map decision 'deny' → request body 'reject', 'allow' →
'once', 'allow-always' → 'always'), validate this._openCodeSessionId and abort
early (or throw) if it's null/empty instead of embedding an empty string in the
URL, perform the POST and check the fetch result (use the promise and inspect
response.ok/status rather than .catch(()=>{}) so network/HTTP errors are not
silently swallowed), and only call this.fire({ type: 'chat:approval-response',
... }) after a successful POST (or include error details in a failure event/log)
so callers don't believe the approval succeeded when it didn't.
- Around line 581-588: deltaForPart currently returns rawDelta when it's a
string but doesn't update the _partText cache, causing later events without
explicit delta to re-emit full text; to fix, compute id via textPartId(part) and
next from part.text, update this._partText.set(id, next) before returning when
typeof rawDelta === 'string' (or move the cache update to run unconditionally at
the top of deltaForPart), leaving the rest of the logic (prev lookup and
slicing) intact so subsequent calls use the correct cached value.
- Around line 346-359: Add clarifying comments to createSession and forkSession
that mirror the existing resumeSession note: state these are intentional
stubs/no-ops because the real OpenCode session is created in connect() by
createOpenCodeSession() which POSTs to /session on the OpenCode REST API;
mention that createSession returns the configured sessionId or a random id as a
placeholder and forkSession generates a random placeholder id, but neither
interacts with the OpenCode backend so callers should not expect them to create
or manage real sessions.

---

Outside diff comments:
In `@server/protocol-adapters/opencode-adapter.ts`:
- Around line 519-542: handleSessionStatus currently returns early on status ===
'error' leaving _currentTurnId intact and not emitting a chat:turn-completed;
update handleSessionStatus to mirror handleSessionError by, when status ===
'error', checking if this._currentTurnId is set, firing a chat:turn-completed
event (include the same fields used by handleSessionError: turnId:
this._currentTurnId, reason: 'failed' or 'error' consistent with
handleSessionError, durationMs, toolCallCount, messageCount), clearing
this._currentTurnId, and then firing the session-status/error event so the turn
is closed and subsequent events are not attributed to the stale turn.

---

Nitpick comments:
In `@server/protocol-adapters/hermes-adapter.ts`:
- Around line 266-274: The respondToInput function extracts firstAnswer but
never uses it, leaving misleading dead code; remove the unused extraction
(firstAnswer) and either leave the function as an explicit no-op with a
clarifying comment or replace it with a clear unsupported operation signal
(e.g., throw or return a specific "not supported" response) so callers/readers
of respondToInput know input handling is intentionally unsupported by the Hermes
gateway.

In `@server/protocol-adapters/opencode-adapter.ts`:
- Around line 82-108: The current use of getPort() plus spawn creates a TOCTOU
race; modify the startup flow so opencode is allowed to bind the port and the
adapter reads the actual bound port from the child process output instead of
preallocating one. Concretely: when building args in the constructor (see
command, defaultArgs, args) allow passing '--port', '0' (or detect a {{PORT}}
placeholder) so the server selects a free port, then listen to
this._process.stdout for the CLI line that reports the bound port (parse and
assign this._apiPort and recompute this._endpoint) before resolving readiness;
as a fallback keep the existing getPort() behavior. Also update waitForServer to
detect an EADDRINUSE error emitted by the child (or printed to stderr) and
surface a distinct, actionable error so races are distinguishable from slow
startups.

In `@test/fixtures/hermes-gateway-stub.cjs`:
- Around line 44-56: Wrap the JSON.parse(body) inside a try/catch in the
req.on('end') handler to prevent the stub from crashing on malformed bodies; on
parse failure log the error and return an HTTP error response (e.g., 400)
instead of calling emitTurn, referencing the req.on('end') callback, the local
variable body, and the emitTurn(res, data.input) call so you only call emitTurn
when parsing succeeds.

In `@test/fixtures/opencode-serve-stub.cjs`:
- Around line 65-76: Wrap the synchronous JSON.parse inside the drain callback
in a try/catch and return a 400 JSON response on parse errors; after parsing
also defensively check that the parsed value is a non-null object and that
payload.parts is an array (use Array.isArray(payload.parts)), returning the same
400 JSON error if those validations fail, then only proceed to writeHead(204),
res.end(), and call emitTurn(); update the block around drain(req, (body) => {
... }) accordingly to avoid throwing on malformed bodies or non-object payloads.

In `@test/opencode-adapter.e2e.test.ts`:
- Around line 17-19: The test creates onBackendStateChanged using a double-cast
(vi.fn() as unknown as (session: Session) => void) which bypasses type checking;
replace this with a properly typed mock by using Vitest’s generic form
vi.fn<ReturnType, ArgTypes>()—specifically change the creation of
onBackendStateChanged to use vi.fn<void, [Session]> (or vi.fn<(session: Session)
=> void>) so the mock is correctly typed without the unsafe as unknown as cast.
- Around line 49-53: The test uses vi.waitFor without an explicit timeout which
can flake on slow CI; update the vi.waitFor call (the block asserting
session.agentState and events) to include a generous explicit timeout option
(e.g., 3000–10000 ms) so the waitFor for session.agentState and the chat events
has more time on CI; keep the same assertions (session.agentState, events.some
for 'chat:text-delta' and 'chat:turn-completed') but pass the timeout option to
vi.waitFor to ensure robustness.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f63897b-b297-4552-a484-8299fecc6afe

📥 Commits

Reviewing files that changed from the base of the PR and between e520f78 and 5cd9fe1.

📒 Files selected for processing (5)
  • server/protocol-adapters/hermes-adapter.ts
  • server/protocol-adapters/opencode-adapter.ts
  • test/fixtures/hermes-gateway-stub.cjs
  • test/fixtures/opencode-serve-stub.cjs
  • test/opencode-adapter.e2e.test.ts

Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/hermes-adapter.ts
Comment thread server/protocol-adapters/opencode-adapter.ts
Comment thread server/protocol-adapters/opencode-adapter.ts
Comment thread server/protocol-adapters/opencode-adapter.ts
@donovan-yohan
donovan-yohan merged commit 057e303 into nightly Apr 24, 2026
5 of 6 checks passed
@donovan-yohan
donovan-yohan deleted the dy/fix-web-session-adapters branch June 10, 2026 15:43
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.

2 participants