Skip to content

fix(opencode-plugin): use @relaycast/sdk instead of dead bespoke RPC API#1190

Merged
willwashburn merged 6 commits into
mainfrom
opencode-plugin-use-relaycast-sdk
Jun 23, 2026
Merged

fix(opencode-plugin): use @relaycast/sdk instead of dead bespoke RPC API#1190
willwashburn merged 6 commits into
mainfrom
opencode-plugin-use-relaycast-sdk

Conversation

@willwashburn

@willwashburn willwashburn commented Jun 23, 2026

Copy link
Copy Markdown
Member

Problem

The OpenCode Relay plugin (plugins/opencode-relay-plugin) talked to a bespoke RPC API at https://www.relaycast.dev/api/v1 (/register, /dm/send, /inbox/check, /agent/list, /message/post, /agent/add, /agent/remove). That host is dead (returns HTTP 522), so the plugin is fully broken.

Change

Transport swap only — same tool names, arguments, return shapes, and idle inbox-polling behavior. The plugin now uses the published @relaycast/sdk (^4.1.6) pointed at the current engine.

  • Default base URL → https://cast.agentrelay.com (old www host is dead; no data continuity). Still configurable via the existing RelayState.apiBaseUrl, now routed into the SDK as baseUrl.
  • Removed the bespoke fetch / normalizeBaseUrl plumbing.
  • Tests now mock the SDK (RelayCast / AgentClient) instead of the dead HTTP URLs, with equivalent coverage (19 tests).

Endpoint → SDK mapping

Bespoke endpoint SDK call
/register new RelayCast({ apiKey: workspace, baseUrl }).registerOrRotate({ name }) → then relay.as(token) for the agent client
/dm/send AgentClient.dm(to, text)
/inbox/check AgentClient.inbox() flattened by inboxToMessages()
/agent/list RelayCast.agents.list()
/message/post AgentClient.post(channel, text)
/agent/add RelayCast.agents.registerOrGet({ name, metadata: { cli, task } })
/agent/remove RelayCast.agents.delete(name)

Things that didn't map cleanly

  • Inbox shape. /v1/inbox returns { unread_channels, mentions, unread_dms, recent_reactions } — no messages[]. inboxToMessages() flattens channel mentions[] and unread_dms[] (using each conversation's lastMessage) into the flat RelayMessage[] the inbox tool and idle hook expect. unread_channels and recent_reactions are not message-like and are ignored.
  • /agent/add (worker registration). The SDK's agents.spawn requires a cli enum (claude|codex|gemini|aider|goose) that excludes opencode, and it is meant to spawn a remote process. The plugin instead spawns a local OpenCode process, so the relay-side step is just registering the worker identity — implemented with agents.registerOrGet({ name, metadata: { cli: 'opencode', task } }). The local spawn(...) behavior, bootstrap prompt, and env vars are unchanged.
  • relay_agents return type. Previously passed through whatever the bespoke API returned; now returns the SDK's Agent[] objects. Shape ({ agents: [...] }) is preserved.

Build / test

  • npm run build (tsc): passes
  • npm test (vitest): 19/19 passing
  • Validated against @relaycast/sdk@4.1.6.

A standalone vitest.config.ts was added to the plugin so its tests run in isolation (the plugin is not part of the monorepo npm workspaces; without it vitest climbs to the root config which can't resolve in a fresh plugin install).

🤖 Generated with Claude Code

Review in cubic

The OpenCode Relay plugin called a bespoke RPC API at
https://www.relaycast.dev/api/v1 (/register, /dm/send, /inbox/check,
/agent/list, /message/post, /agent/add, /agent/remove). That host is dead
(HTTP 522). This rewrites the transport to the published @relaycast/sdk
pointed at the current engine (https://cast.agentrelay.com), preserving the
plugin's public contract (tool names, args, return shapes, idle-poll hook).

Endpoint -> SDK mapping:
- /register     -> RelayCast({apiKey: workspace}).registerOrRotate({name})
                   then relay.as(token) for the agent-scoped client
- /dm/send      -> AgentClient.dm(to, text)
- /inbox/check  -> AgentClient.inbox(); flattened via inboxToMessages()
                   (engine inbox has no `messages[]`; mentions[] + unread_dms[]
                   are the message-like items)
- /agent/list   -> RelayCast.agents.list()
- /message/post -> AgentClient.post(channel, text)
- /agent/add    -> RelayCast.agents.registerOrGet({name, metadata:{cli,task}})
                   (agents.spawn requires a cli enum that excludes "opencode",
                   so we register the identity and spawn the local process)
- /agent/remove -> RelayCast.agents.delete(name)

- Default base URL is now https://cast.agentrelay.com; still configurable via
  RelayState.apiBaseUrl, routed into the SDK as baseUrl.
- Removed the bespoke fetch/normalizeBaseUrl plumbing.
- Tests now mock the SDK (RelayCast/AgentClient) instead of the dead HTTP URLs.
- Added a standalone vitest.config.ts so the plugin tests run in isolation
  (the plugin is not part of the monorepo npm workspaces).

Build (tsc) and tests (vitest, 19 passing) both green against
@relaycast/sdk@4.1.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 439c0af2-f8cc-434a-b36d-98598f3d93db

📥 Commits

Reviewing files that changed from the base of the PR and between f5c8a99 and 78d8785.

📒 Files selected for processing (5)
  • plugins/opencode-relay-plugin/src/index.ts
  • plugins/opencode-relay-plugin/src/tools.ts
  • plugins/opencode-relay-plugin/tests/mock-relay-server.ts
  • plugins/opencode-relay-plugin/tests/polling.test.ts
  • plugins/opencode-relay-plugin/tests/tools.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • plugins/opencode-relay-plugin/tests/polling.test.ts
  • plugins/opencode-relay-plugin/tests/tools.test.ts
  • plugins/opencode-relay-plugin/tests/mock-relay-server.ts
  • plugins/opencode-relay-plugin/src/index.ts

📝 Walkthrough

Walkthrough

The opencode-relay-plugin is migrated from raw HTTP fetch calls against relay REST endpoints to using the @relaycast/sdk client. RelayState gains relay and agent fields; all tool handlers (relay_connect, relay_send, relay_inbox, relay_agents, relay_post, relay_spawn, relay_dismiss) and pollInbox in hooks are updated to use SDK methods. Test infrastructure replaces global fetch stubs with an in-memory MockRelayCast/MockAgentClient layer.

Changes

SDK Migration for opencode-relay-plugin

Layer / File(s) Summary
Dependencies and build configuration
plugins/opencode-relay-plugin/package.json, plugins/opencode-relay-plugin/vitest.config.ts
Adds @relaycast/sdk as a dependency; updates npm test script to pass --config vitest.config.ts; creates plugin-level Vitest configuration with Node environment and tests/**/*.test.ts discovery.
RelayState extension, API base URL, and SDK imports
plugins/opencode-relay-plugin/src/index.ts, plugins/opencode-relay-plugin/src/tools.ts
Extends RelayState with relay: RelayCast | null and agent: AgentClient | null fields; updates DEFAULT_RELAYCAST_API_BASE_URL to https://cast.agentrelay.com; swaps module re-exports to add inboxToMessages/RelayCastFactory and remove relaycastAPI/RelayAPIResponse; adds SDK client imports.
Type contracts and type guards with dependency injection
plugins/opencode-relay-plugin/src/tools.ts
Introduces SpawnLike and RelayCastFactory types; expands ToolDependencies with optional createRelayCast; updates isConnected and assertConnected to enforce relay: RelayCast and agent: AgentClient when connected; implements inbox utilities (inboxToMessages, collectInboxMessages, rememberSeen, MAX_SEEN_MESSAGE_IDS) with defensive null handling and message hydration.
Relay connection and authorization handling
plugins/opencode-relay-plugin/src/tools.ts
Refactors relay_connect to use injected createRelayCast for SDK client instantiation, calls registerOrRotate for token acquisition, translates authorization failures via isAuthError, validates token presence, and populates RelayState with client references and metadata; passes dependencies into createRelayConnectTool.
SDK-backed tool handlers
plugins/opencode-relay-plugin/src/tools.ts
Switches relay_send to agent.dm(); relay_inbox to agent.inbox() + collectInboxMessages; relay_agents to relay.agents.list(); relay_post to agent.post(); relay_spawn to relay.agents.registerOrGet() with metadata before process spawn; relay_dismiss to relay.agents.delete().
Hook-based inbox polling switched to SDK agent
plugins/opencode-relay-plugin/src/hooks.ts
Removes InboxCheckResponse type; gates handleSessionIdle on state.agent instead of state.token; replaces pollInbox HTTP fetch with state.agent.inbox() + collectInboxMessages, returning empty array when no agent is available.
In-memory mock SDK server
plugins/opencode-relay-plugin/tests/mock-relay-server.ts
Rebuilds MockRelayServer as in-memory relaycast with message/agent storage, registerShouldFail flag, record(), injection helpers, durable delivery ledger for read tracking, and buildInbox(); adds MockAgentClient and MockRelayCast implementing SDK interfaces; exports createMockRelayCastFactory; updates connectRelayState to wire RelayState directly to MockRelayCast.
Core tools test suite updated for SDK mocking
plugins/opencode-relay-plugin/tests/tools.test.ts
Removes global fetch stubbing; imports createMockRelayCastFactory and utility exports; injects mock factory into relay_connect via ToolDependencies; initializes relay state with connectRelayState(state, server) throughout; simplifies afterEach cleanup; adds/updates inbox hydration, message draining, and read acknowledgement assertions; adds rememberSeen unit tests and defensive inboxToMessages tests.
Polling test suite with inbox callback mocking
plugins/opencode-relay-plugin/tests/polling.test.ts
Removes global fetch stubbing; imports InboxResponse type; introduces emptyInbox() and createConnectedState helpers to wire agent.inbox mock callback; updates afterEach to use vi.restoreAllMocks(); rewrites idle-no-messages and idle-surfaces-messages with inbox call count assertions and structured InboxResponse payloads; adds idle-drains-and-does-not-reinject and idle-swallows-errors test cases; updates compacting-preserves and end-cleanup setup.
Integration and spawn test suites with mock server wiring
plugins/opencode-relay-plugin/tests/integration.test.ts, plugins/opencode-relay-plugin/tests/spawn.test.ts
Integration tests remove unused imports (relayPlugin, createMockFetch, createPluginContext), import createMockRelayCastFactory, simplify afterEach, inject mock factory into relay_connect handler, and pass server into connectRelayState. Spawn tests remove createMockFetch import, pass server into all connectRelayState calls, and simplify afterEach to only restore mocks.

Sequence Diagram(s)

sequenceDiagram
  participant Tool as relay_connect Tool
  participant Factory as RelayCastFactory
  participant RC as RelayCast
  participant AC as AgentClient
  participant RS as RelayState

  Tool->>Factory: createRelayCast(apiKey)
  Factory-->>Tool: RelayCast instance
  Tool->>RC: registerOrRotate(agentName)
  RC-->>Tool: token or RelayError
  alt isAuthError(error)
    Tool-->>Tool: return invalid workspace key error
  end
  Tool->>RC: as(agentName)
  RC-->>Tool: AgentClient
  Tool->>RS: relay=RC, agent=AC, token, workspace, agentName, connected=true

  Note over Tool,RS: Subsequent tool calls use state.relay and state.agent

  Tool->>AC: agent.dm(to, text)
  Tool->>AC: agent.inbox()
  Tool->>RC: relay.agents.list()
  Tool->>AC: agent.post(channel, text)
  Tool->>RC: relay.agents.registerOrGet(name, metadata)
  Tool->>RC: relay.agents.delete(name)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/relay#1133: Pins @relaycast/sdk to 4.0.0 in the monorepo, which is the SDK version this PR adds as a direct dependency to opencode-relay-plugin.
  • AgentWorkforce/relay#1074: Updates @relaycast/sdk registration APIs and introduces typed error handling for registerOrRotate, which is the SDK method now used by the relay_connect handler in this PR.

Suggested reviewers

  • khaliqgant

Poem

🐇 No more fetch calls through the night,
The SDK hops in, clean and bright.
relay, agent — fields so new,
MockRelayCast fakes the queue.
The inbox polls with style and grace,
A rabbit's code runs at full pace! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.33% 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
Title check ✅ Passed The title accurately and clearly describes the main change: replacing a dead bespoke RPC API with the @relaycast/sdk.
Description check ✅ Passed The description comprehensively covers the problem, solution, endpoint mappings, implementation details, and test status, exceeding the basic template requirements.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opencode-plugin-use-relaycast-sdk

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the opencode-relay-plugin to use the @relaycast/sdk instead of making direct HTTP fetch calls, updating the plugin state, hooks, tools, and tests accordingly. Feedback on these changes suggests adding defensive checks in inboxToMessages to handle potentially missing or nullish inbox properties, and safely accessing the token from the registration response in relay_connect to prevent runtime errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 111 to 137
export function inboxToMessages(inbox: InboxResponse): RelayMessage[] {
const messages: RelayMessage[] = [];

for (const mention of inbox.mentions) {
messages.push({
id: mention.id,
from: mention.agentName,
text: mention.text,
channel: mention.channelName,
ts: mention.createdAt,
});
}

if (!res.ok) {
throw new Error(`Relay API error: ${res.status} ${await res.text()}`);
for (const dm of inbox.unreadDms) {
if (!dm.lastMessage) {
continue;
}
messages.push({
id: dm.lastMessage.id,
from: dm.from,
text: dm.lastMessage.text,
ts: dm.lastMessage.createdAt,
});
}

return (await res.json()) as RelayAPIResponse;
return messages;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To prevent potential runtime errors, it is highly recommended to defensively handle cases where inbox is null/undefined, or where mentions and unreadDms are missing or not arrays. Using optional chaining and nullish coalescing ensures the plugin remains robust against unexpected API or SDK responses.

export function inboxToMessages(inbox: InboxResponse): RelayMessage[] {
  const messages: RelayMessage[] = [];

  if (!inbox) {
    return messages;
  }

  for (const mention of inbox.mentions ?? []) {
    messages.push({
      id: mention.id,
      from: mention.agentName,
      text: mention.text,
      channel: mention.channelName,
      ts: mention.createdAt,
    });
  }

  for (const dm of inbox.unreadDms ?? []) {
    if (!dm.lastMessage) {
      continue;
    }
    messages.push({
      id: dm.lastMessage.id,
      from: dm.from,
      text: dm.lastMessage.text,
      ts: dm.lastMessage.createdAt,
    });
  }

  return messages;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 52a2e89. inboxToMessages now accepts InboxResponse | null | undefined and guards both arrays with Array.isArray(inbox?.mentions) ? ... : [] (same for unreadDms), returning [] rather than throwing on a null/partial payload during an idle poll. The same defensive guards are applied in the new collectInboxMessages. Covered by the new inboxToMessages defensive handling tests (null/undefined inbox and non-array fields).

Comment on lines +173 to +174
const registration = await relay.registerOrRotate({ name });
token = registration.token;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Defensively handle cases where registration might be null or undefined to avoid throwing a TypeError when accessing .token.

Suggested change
const registration = await relay.registerOrRotate({ name });
token = registration.token;
const registration = await relay.registerOrRotate({ name });
token = registration?.token;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 52a2e89. Now uses registration?.token (and token is typed string | undefined), so a null/undefined registration response no longer throws a TypeError on property access — it falls through to the existing typeof token !== "string" || token.length === 0 guard which raises the clear register response missing token error.

@willwashburn willwashburn marked this pull request as ready for review June 23, 2026 12:37
@willwashburn willwashburn requested a review from khaliqgant as a code owner June 23, 2026 12:37

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5f693b575

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +233 to +234
const inbox = await state.agent.inbox();
const messages = inboxToMessages(inbox);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark surfaced inbox messages as read

When there is any unread DM or mention, this call leaves it unread; the SDK inbox endpoint is read-only and the SDK exposes markRead separately, so relay_inbox and the session.idle poll will keep returning/injecting the same message every poll instead of matching the old /inbox/check drain behavior. Mark the surfaced message ids read, or maintain an equivalent seen watermark, after surfacing them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 52a2e89. Surfaced messages are now drained: collectInboxMessages calls agent.markRead(id) (the SDK read-receipt API) for each surfaced mention/DM/channel message, and also records the id in a persistent state.seenMessageIds watermark as a backstop so a message is never re-injected even if the markRead write lags or fails. Both relay_inbox and the session.idle poll go through this path, so the read-only engine inbox no longer re-surfaces the same items each poll. Covered by new tests drains surfaced inbox messages via markRead... (tools) and idle-drains-and-does-not-reinject (polling).

Comment on lines +124 to +129
for (const dm of inbox.unreadDms) {
if (!dm.lastMessage) {
continue;
}
messages.push({
id: dm.lastMessage.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fetch all unread DM messages before flattening

When a DM conversation has unreadCount > 1, the SDK inbox summary only includes lastMessage, and this loop emits just that one message. Earlier unread DMs from the same sender are never returned by relay_inbox or idle injection despite the old flat messages[] behavior, so multi-message instructions can be silently lost; fetch the conversation's unread messages or otherwise preserve the missing messages before flattening.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 52a2e89. For DM conversations with unreadCount > 1, collectInboxMessages now hydrates the unread tail via agent.dms.messages(conversationId, { limit }) (capped at 50) and restores chronological order, instead of only taking lastMessage. If the fetch fails or returns nothing it falls back to the summary lastMessage. Covered by the new test hydrates earlier messages of a multi-message DM conversation.

Comment on lines +114 to +120
for (const mention of inbox.mentions) {
messages.push({
id: mention.id,
from: mention.agentName,
text: mention.text,
channel: mention.channelName,
ts: mention.createdAt,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface unread channel posts, not just mentions

For channel posts that do not explicitly mention the agent, the SDK inbox records them under unreadChannels, but this flattening path only returns mentions and DMs. That means messages sent with relay_post will not appear in another agent's relay_inbox or idle prompt unless they contain a mention, regressing the previous channel-message queue behavior; include unread channel summaries or fetch the unread channel messages before returning an empty inbox.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 52a2e89. collectInboxMessages now iterates inbox.unreadChannels and, for each channel with unread posts, fetches them via agent.messages(channelName, { limit }) (capped at 50) and surfaces them with their channel set. This restores the previous behavior where non-mention relay_post messages reach another agents inbox/idle injection. Deduped against mentions and drained via the same markRead+ watermark path. Covered by the new testsurfaces unread channel posts that did not mention the reader`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/opencode-relay-plugin/tests/mock-relay-server.ts`:
- Around line 15-20: The QueuedDm interface is missing the channel property,
which causes the filter check at line 50 that uses 'channel' in m to always fail
for queued items. Add a channel property to the QueuedDm interface to match the
structure of messages that need to be filtered for mentions, and ensure the
injectMessage function preserves the channel field when creating queued items
instead of dropping it. This will allow mention payloads to be properly
identified and tested through the shared mock helper.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b80a5815-6b6a-4a78-a666-0f266096134a

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb86d0 and c5f693b.

📒 Files selected for processing (10)
  • plugins/opencode-relay-plugin/package.json
  • plugins/opencode-relay-plugin/src/hooks.ts
  • plugins/opencode-relay-plugin/src/index.ts
  • plugins/opencode-relay-plugin/src/tools.ts
  • plugins/opencode-relay-plugin/tests/integration.test.ts
  • plugins/opencode-relay-plugin/tests/mock-relay-server.ts
  • plugins/opencode-relay-plugin/tests/polling.test.ts
  • plugins/opencode-relay-plugin/tests/spawn.test.ts
  • plugins/opencode-relay-plugin/tests/tools.test.ts
  • plugins/opencode-relay-plugin/vitest.config.ts

Comment thread plugins/opencode-relay-plugin/tests/mock-relay-server.ts
willwashburn and others added 2 commits June 23, 2026 09:00
…essage DMs/channel posts

The engine `GET /v1/inbox` is read-only and returns a summary, unlike the old
`/inbox/check` which drained a queue. Faithfully replicate the drain behavior:

- Drain surfaced messages via `markRead` and a local `seenMessageIds`
  watermark, so `relay_inbox` and the `session.idle` poll no longer re-surface
  the same messages every poll. (Codex P1)
- Hydrate multi-message DM conversations (`unreadCount > 1`) from
  `dms.messages` so earlier unread DMs are not dropped by the summary's
  `lastMessage`-only view. (Codex P2)
- Surface unread channel posts (under `unreadChannels`) that did not mention
  the reader by fetching them via `messages(channel)`, preserving the old
  channel-message queue behavior. (Codex P2)
- Add defensive null/array guards in `inboxToMessages` and on the registration
  token. (Gemini)

New shared `collectInboxMessages(agent, inbox, seen)` performs the hydrate +
drain and is used by both the inbox tool and the idle hook. The mock relay
server now models a read-only inbox (acked via `markRead`) plus the DM/channel
message APIs; tests cover drain, multi-message DMs, channel posts, and
defensive nulls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/opencode-relay-plugin/src/tools.ts`:
- Around line 278-302: The channel unread count is not being decremented by the
markRead method, causing the same channels to be re-fetched on every idle poll
despite deduplication by the seen watermark. Add a mechanism to track which
channels have already been processed in the current poll cycle. Before calling
fetchUnreadChannelMessages for each channel in the unreadChannels loop, check if
the channel has already been hydrated in this poll using a separate tracking
structure (such as a Set to store processed channel names). Only call
fetchUnreadChannelMessages if the channel is not in the processed set, then add
it to the set after successful hydration. This gates channel re-fetch on whether
it has already been processed in the current cycle, eliminating redundant
network round-trips for the same channels.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba7802cd-a35f-4716-b108-5f6e41aadc3a

📥 Commits

Reviewing files that changed from the base of the PR and between c5f693b and f5c8a99.

📒 Files selected for processing (6)
  • plugins/opencode-relay-plugin/src/hooks.ts
  • plugins/opencode-relay-plugin/src/index.ts
  • plugins/opencode-relay-plugin/src/tools.ts
  • plugins/opencode-relay-plugin/tests/mock-relay-server.ts
  • plugins/opencode-relay-plugin/tests/polling.test.ts
  • plugins/opencode-relay-plugin/tests/tools.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • plugins/opencode-relay-plugin/src/hooks.ts
  • plugins/opencode-relay-plugin/src/index.ts
  • plugins/opencode-relay-plugin/tests/tools.test.ts
  • plugins/opencode-relay-plugin/tests/polling.test.ts
  • plugins/opencode-relay-plugin/tests/mock-relay-server.ts

Comment thread plugins/opencode-relay-plugin/src/tools.ts Outdated
willwashburn and others added 2 commits June 23, 2026 09:37
…watermark

CodeRabbit correctly flagged that @relaycast/sdk's markRead is a no-op
stub for delivery state (it posts a read receipt to /v1/messages/:id/read
and does not change unread/delivery state). The inbox "drain" therefore
relied entirely on the client-side seenMessageIds watermark and the
markRead calls were dead/misleading code.

The SDK does expose a real server-side drain: the durable delivery ledger
(deliveries() + ackDelivery()/failDelivery()/deferDelivery() over
/v1/deliveries). Each DeliveryItem carries the underlying messageId, so
surfaced inbox items map cleanly onto delivery rows.

- Replace the no-op markRead drain with ackSurfacedDeliveries(): list the
  non-terminal delivery queue (capped at MAX_DELIVERY_SCAN), match rows to
  the just-surfaced message ids, and ackDelivery() each. This transitions
  the delivery to `acked` server-side so it stops being replayed across
  restarts (best-effort; the watermark remains the in-session backstop).
- Bound seenMessageIds with FIFO eviction via rememberSeen()
  (MAX_SEEN_MESSAGE_IDS = 2000) so it can't grow unboundedly over a long
  session; older ids are safe to drop since their deliveries are acked.
- Update mock-relay-server to model the delivery ledger (deliveries/ack)
  instead of markRead, and update tests accordingly; add rememberSeen
  bounding tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit 86ad9b4 into main Jun 23, 2026
2 checks passed
@willwashburn willwashburn deleted the opencode-plugin-use-relaycast-sdk branch June 23, 2026 14:07
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