Skip to content

Add @relaycast/openclaw package for OpenClaw integration#3

Merged
khaliqgant merged 6 commits into
mainfrom
claude/openclaw-relaycast-integration-b2Eaa
Feb 9, 2026
Merged

Add @relaycast/openclaw package for OpenClaw integration#3
khaliqgant merged 6 commits into
mainfrom
claude/openclaw-relaycast-integration-b2Eaa

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Feb 8, 2026

Copy link
Copy Markdown
Member

Introduce a new package that makes it dead simple for OpenClaw developers
to add structured multi-claw messaging via Relaycast. Includes:

  • OpenClawBridge class: connects any claw instance to a Relaycast workspace,
    auto-registering as an agent with channel join, DMs, threads, reactions,
    search, and real-time WebSocket events
  • Skill installer: relay openclaw setup writes a SKILL.md and .env into
    ~/.openclaw/workspace/relaycast/ and optionally patches openclaw.json
    with the MCP server config
  • Config detection: auto-discovers OpenClaw installation and parses config
  • CLI integration: relay openclaw setup and relay openclaw status
  • 19 tests covering bridge, config detection, and skill setup

https://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4


Open with Devin

Introduce a new package that makes it dead simple for OpenClaw developers
to add structured multi-claw messaging via Relaycast. Includes:

- OpenClawBridge class: connects any claw instance to a Relaycast workspace,
  auto-registering as an agent with channel join, DMs, threads, reactions,
  search, and real-time WebSocket events
- Skill installer: `relay openclaw setup` writes a SKILL.md and .env into
  ~/.openclaw/workspace/relaycast/ and optionally patches openclaw.json
  with the MCP server config
- Config detection: auto-discovers OpenClaw installation and parses config
- CLI integration: `relay openclaw setup` and `relay openclaw status`
- 19 tests covering bridge, config detection, and skill setup

https://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread packages/openclaw/src/setup.ts Outdated
claude and others added 2 commits February 8, 2026 22:28
Adds a dedicated section showcasing the OpenClaw integration with CLI
and SDK code examples, feature badges, and an OpenClaw entry in the
"works with" tool grid plus nav link.

https://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +144 to +147
const env = ENV_TEMPLATE
.replace('__API_KEY__', options.apiKey)
.replace('__CLAW_NAME__', options.clawName ?? 'my-claw')
.replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Sequential String.replace() on ENV_TEMPLATE allows user input to corrupt .env output

The chained .replace() calls on lines 144-147 use simple string replacement, which means if an earlier replacement value contains a later placeholder token, the later .replace() will match and substitute within the already-replaced content, corrupting the .env file.

Detailed Explanation

The replacement chain is:

ENV_TEMPLATE
  .replace('__API_KEY__', options.apiKey)
  .replace('__CLAW_NAME__', options.clawName ?? 'my-claw')
  .replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev');

If options.apiKey contains the literal string __CLAW_NAME__, the second .replace() will match it inside the already-substituted API key value. For example:

  • API key: rk_test___CLAW_NAME__ → after first replace: RELAY_API_KEY=rk_test___CLAW_NAME__
  • Second replace matches __CLAW_NAME__ inside the API key → RELAY_API_KEY=rk_test_my-claw and RELAY_CLAW_NAME=__CLAW_NAME__ is left unreplaced

Similarly, if clawName contains __BASE_URL__, the third replace corrupts the claw name and leaves RELAY_BASE_URL with the raw placeholder.

While API keys are unlikely to contain these tokens, clawName is user-provided (via CLI --name flag at packages/cli/src/commands/openclaw.ts:29) and could theoretically contain __BASE_URL__.

Impact: The generated .env file would have incorrect values, causing the Relaycast skill to malfunction (wrong API key, wrong claw name, or missing base URL).

Suggested change
const env = ENV_TEMPLATE
.replace('__API_KEY__', options.apiKey)
.replace('__CLAW_NAME__', options.clawName ?? 'my-claw')
.replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev');
const env = [
'# Relaycast configuration for this OpenClaw skill',
`RELAY_API_KEY=${options.apiKey}`,
`RELAY_CLAW_NAME=${options.clawName ?? 'my-claw'}`,
`RELAY_BASE_URL=${options.baseUrl ?? 'https://api.relaycast.dev'}`,
'',
].join('\n');
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@khaliqgant khaliqgant merged commit 544f1b0 into main Feb 9, 2026
1 check passed
@khaliqgant khaliqgant deleted the claude/openclaw-relaycast-integration-b2Eaa branch February 9, 2026 08:56

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

store.setState({ connectionStatus: 'disconnected' });
ws.disconnect();
};
}, [clients, store, channels]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 WebSocket tears down and reconnects on every parent re-render due to unstable channels array reference

The useEffect in RelayProvider includes channels in its dependency array. Because React compares dependencies with Object.is, passing an array literal like channels={['general', 'alerts']} creates a new reference on every parent render, causing the effect to re-run. Each re-run disconnects the WebSocket (ws.disconnect()), then reconnects (ws.connect()), losing all in-flight subscriptions and events.

Root Cause and Impact

At packages/react/src/provider.tsx:58, channels is listed in the dependency array:

}, [clients, store, channels]);

Unlike clients (stabilized via useMemo) and store (created once), channels is a raw prop. A typical consumer writes:

<RelayProvider apiKey="..." agentToken="..." channels={['general']}>

Every render of the parent creates a new ['general'] array, so Object.is(prevChannels, nextChannels) is false. The cleanup runs (lines 51-57), disconnecting the WebSocket and setting status to 'disconnected', then immediately re-runs the setup, reconnecting. This causes connection thrashing, dropped real-time events, and unnecessary network overhead on every unrelated parent re-render.

Prompt for agents
In packages/react/src/provider.tsx, the `channels` prop is an array that will have a new reference on every parent render if passed as a literal. This causes the WebSocket useEffect to tear down and re-run on every parent re-render. Fix this by either: (1) removing `channels` from the useEffect dependency array and instead using a ref to track the latest channels value, sending subscribe/unsubscribe only when channels actually change (via a separate useEffect with a JSON.stringify or deep comparison), or (2) memoizing the channels internally using a custom hook like: `const stableChannels = useRef(channels); stableChannels.current = channels;` and only including stable deps in the main useEffect, while handling channel subscription changes in a separate effect.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +34 to +44
useEffect(() => {
if (!state.loading) return;
ctx.agent.dms.conversations()
.then((data) => {
store.setState({ dms: { data, loading: false, error: null } });
})
.catch(() => {
// Silently fail on refetch — keep existing data
store.setState({ dms: { ...store.getState().dms, loading: false } });
});
}, [state.loading, ctx.agent, store]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 useDMs double-fetches on mount because the first effect's loading: true triggers the second refetch effect

The useDMs hook has two useEffects that both fire API requests on initial mount, resulting in two concurrent calls to ctx.agent.dms.conversations().

Root Cause and Impact

The first effect (line 10) synchronously sets loading: true and starts a fetch. The second effect (line 34) watches state.loading and fires a refetch whenever it becomes true — intended only for DM event notifications via handleDmReceived (packages/react/src/reducer.ts:306-310).

On mount:

  1. First useEffect (line 10-26): calls store.setState({ dms: { ...dms, loading: true } }) then fires fetch A.
  2. React re-renders because useSyncExternalStore detects dms changed.
  3. Second useEffect (line 34-44): sees state.loading === true and fires fetch B.
  4. Both fetch A and fetch B are now in-flight concurrently.

This causes a race condition where both responses overwrite each other, and the API is called twice unnecessarily on every mount. If the API is rate-limited or slow, this can cause visible UI flicker (loading → data → loading → data) and wasted network requests.

Prompt for agents
In packages/react/src/hooks/useDMs.ts, the second useEffect (lines 34-44) is intended to refetch DMs when a dm.received event sets loading to true, but it also fires on the initial mount because the first useEffect also sets loading to true. Fix this by adding a ref to skip the initial trigger, e.g.:

const mountedRef = useRef(false);
useEffect(() => {
  if (!mountedRef.current) { mountedRef.current = true; return; }
  if (!state.loading) return;
  // ... refetch logic ...
}, [state.loading, ctx.agent, store]);

Alternatively, use a distinct flag (like a 'stale' boolean) instead of reusing 'loading' for both initial load and event-driven refetches.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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