Skip to content

v8 SDK redesign: live agent clients, addListener, fire-and-forget actions, webhooks#1029

Merged
willwashburn merged 21 commits into
mainfrom
claude/upbeat-tesla-zqeKw
Jun 3, 2026
Merged

v8 SDK redesign: live agent clients, addListener, fire-and-forget actions, webhooks#1029
willwashburn merged 21 commits into
mainfrom
claude/upbeat-tesla-zqeKw

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Jun 3, 2026

User description

Summary

Implements the v8 @agent-relay/sdk API redesign — the surface designed in the README is now real, end-to-end. The redesign was driven through a sequence of locked decisions (#1#15) and built in phases, each landing green (typecheck + tests across sdk, cli, openclaw, harnesses).

The headline shift: agents are live clients, not token records; one listener entry point; fire-and-forget actions through the relay; and a dedicated webhooks namespace — all on top of @relaycast/sdk 2.3.x (the v8 service contract).

What changed

Client model

  • relay.workspace.register(...) returns a live agent client (identity + status.becomes(...)/tools.called(...) predicates + agent-scoped messaging). Single agent in → one client; array in → an array. Duplicate names are rejected.
  • relay.workspace.reconnect({ apiToken }) rehydrates a client from a persisted token (agents.me() → relaycast GET /v1/agent).
  • Removed AgentRelay.as() / asAgent().

Messaging

  • Sends come from a registered participant: agent.sendMessage({ to }) (#channel / @handle / ['@a','@b'] group DM), agent.reply({ messageId }), agent.react({ messageId, emoji }).
  • Every message exposes messageId. Removed the top-level relay.sendMessage (no system sender).

Events

  • relay.addListener(selector, handler) is the sole entry point — dotted event name, */prefix wildcard, or a predicate. Handlers get one discriminated event object; message events carry a rich envelope (from/to/channel/parent).
  • Canonical dotted vocabulary shared with webhook subscriptions. Removed relay.on / relay.notify.

Actions (fire-and-forget)

  • relay.registerAction(...) registers a descriptor on the relay and runs the handler on action.invoked (loads input via getInvocation, runs the local ActionRegistry as validator/executor, posts completeInvocation). Results surface as action.completed events; the invoking agent gets an ack, not the return value. The MCP invokes through the relay. Removed the public relay.actions namespace.

Harnesses & humans

  • createHuman({ relay, name }) self-registers and returns the live client; defineHarness re-exported from @agent-relay/harnesses. AgentSession.release is optional, paired with capabilities.lifecycle.release.

Webhooks

  • New relay.webhooks namespace: createInbound({ channel }){ url, token }; subscribe({ url, events, secret, headers }) (HMAC). relay.integrations left intact for provider connections.

Supporting

  • Bumped @relaycast/sdk to 2.3.x; migrated openclaw to the unified message.reacted event.
  • README + events.mdx are the spec of record; 20 web/content/docs/*.mdx pages rewritten (incl. migration.mdx as a from-old/to-v8 guide); CHANGELOG updated with Breaking Changes + Migration Guidance.

Testing

  • Unit tests + mocks across the SDK (register/reconnect, addListener + envelope, fire-and-forget actions, webhooks, group-DM routing) and consumers; full suite green.
  • Added tests/integration/sdk/v8-api-smoke.mjs — a live-relay smoke script for the new surface.

Not yet verified

The action/webhook wiring is verified against mocks + the relaycast openapi/engine source, but not against a running broker. Running node tests/integration/sdk/v8-api-smoke.mjs against a live workspace is the remaining validation step. The matching service contract was filed as AgentWorkforce/relaycast#158 and shipped in @relaycast/sdk 2.3.0.

🤖 Generated with Claude Code


CodeAnt-AI Description

Align the SDK and docs with the v8 client, listener, action, and webhook flow

What Changed

  • Registering an agent now returns a live client you can send from, and that client can be reconnected later from its saved token
  • Messages now use messageId for replies and reactions, and to can route to a channel, a direct message, or a group DM
  • Event handling now goes through one addListener entry point with dotted event names, wildcards, or predicates, and message events include a richer sender/channel envelope
  • Actions now run as fire-and-forget invocations with async completion events, and webhooks now support inbound channel posting plus outbound event delivery
  • README, docs, and examples were rewritten to match the new API, including harness setup and human registration

Impact

✅ Fewer broken examples for v8 users
✅ Clearer agent messaging and thread replies
✅ Easier event and webhook integration

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

willwashburn and others added 18 commits June 2, 2026 17:41
Refresh README to reflect SDK/API changes and simplify examples. Rename persisted key option to workspaceKey and switch agent registration to relay.workspace.register (returns agent objects instead of tokens). Replace old token/as() flow and multiplexed websocket example with simplified realtime listeners (relay.addListener), show messageId usage and thread/emoji reply examples. Add new harness examples (claude/codex, createHuman), update defineHarness sample to use explicit capabilities, and move/condense harness contract content. Update event and action APIs (use addListener for actions), add webhook usage and outbound webhook subscription examples, and remove large duplicated harness internals for a more concise developer guide.
Align the README with the resolved SDK API design:
- create({relay}) owns spawn+register; drop redundant register([...]) call
- register()/create() return the live client; remove as(); add reconnect note
- single addListener entry point with dotted event names, wildcards, predicates
- one discriminated event object; rich message envelope (from/to/channel objects)
- messageId everywhere; sendMessage to-routing (#channel, @dm, [group dm])
- reply/react as the thread/reaction verbs (drop sendEmoji, sendMessage parent)
- no system sender; messages come from real participants
- relay.webhooks namespace (createInbound/subscribe), HMAC outbound
- defineHarness from @agent-relay/harnesses; release required iff capability set
- createHuman({relay}) self-registers
- fix compile bugs/typos in examples

Add web/content/docs/events.mdx pinning the canonical event vocabulary and
envelope schema, wired into docs nav.
Replace the local-MCP-colocated, return-value-to-agent action model with an
event-driven one:
- descriptor (name + input schema) stored on the relay; the agent's MCP reads it
- invoking an action returns an immediate ack; the handler runs in whichever SDK
  process registered it (can live anywhere) and emits action.completed/failed
- results go to SDK listeners only — the handler messages the invoking agent if
  it needs the outcome (per the voting/spawn examples)

Update README actions section + spawn example and add an Action lifecycle
section to the events reference.
2.3.0 ships the SDK v8 service contract from relaycast#158: reconnect/me
(resolve-by-token), webhooks.createInbound returning {url, token} with
bearer-authed trigger, subscription header passthrough, and the dotted event
vocabulary. Typecheck and SDK tests pass unchanged.
Condense harness/capability/listener/action notes, move explanatory asides into
NOTE callouts.
2.3.0 replaced the separate reactionAdded/reactionRemoved events with a single
messageReacted event carrying an optional action ('added' | 'removed'). Collapse
the gateway's two subscriptions into one and update the test mocks/fixtures.
workspace.register(...) now returns a live RelayAgentClient instead of a token
record: it carries the agent's identity (id/name/handle), status/tool predicate
builders, and an agent-scoped messaging surface with sendMessage/reply/react
keyed on messageId. A single agent in returns a single client; an array returns
an array.

- Add workspace.reconnect({ apiToken }) to rehydrate a client from a persisted
  token, resolving identity via the new agents.me() (relaycast GET /v1/agent).
- Add messageId to RelayMessage (mirrors id) so sends/replies expose it.
- Add an injectable createAgentMessaging factory for token-scoped clients
  (also makes register/reconnect unit-testable).
- Deprecate as()/asAgent() in favor of register/reconnect.
- Remove as()/asAgent() from AgentRelay; the live client now comes only from
  workspace.register/reconnect. Migrate the CLI's createAgentRelay to construct
  an agent-token-scoped AgentRelay directly.
- sendMessage `to` accepts an array of @Handles, routing to a group DM via
  messages.groupDirect (single #channel / @handle routing unchanged).
Single listener entry point: addListener(selector, handler) where selector is a
dotted event name, a '*'/prefix wildcard, or a predicate. Every handler receives
one discriminated RelayEvent object; message events carry a rich envelope
(from/to/channel/parent).

Maps the camelCase raw messaging events to the canonical dotted vocabulary
(message.created/updated/read/reacted, thread.reply, dm.received), passes through
action.* events (caller -> agent), and lifts session status.* to agent.status.*.
relay.on/notify remain for now; their removal is the next step.
addListener is now the sole listener entry point; the fluent predicate builders
(events.message.created(), action(name).calledBy(), status.becomes(),
tools.called()) live on as inputs to it. relay.on/relay.notify had no production
callers; migrate the listener tests to addListener.

The relay.actions register/invoke namespace is left in place for now — it still
backs the MCP/test invocation path and needs separate handling.
relay.registerAction(...) is the only public action API; the internal
ActionRegistry is no longer exposed as relay.actions. Tests that triggered
actions via relay.actions.invoke now hold their own ActionRegistry instance.
The workspace AgentRelay no longer exposes sendMessage — messages are sent from a
registered participant. The live agent client (RelayAgentClient from
register/reconnect) keeps sendMessage, wired explicitly in assembleAgentClient.
Migrate the routing tests and the README contract test to send from a registered
agent client.
createHuman({ relay, name }) self-registers a human participant and returns the
live client, mirroring claude.create({ relay }). Re-export defineHarness,
normalizeAgentIdentity, MINIMAL_AGENT_SESSION_CAPABILITIES and the harness
contract types from @agent-relay/sdk so custom-harness authors have one import.
…e 3b)

lifecycle.release is now boolean (was the literal true), and AgentSession.release
is optional: provide it iff capabilities.lifecycle.release is true, omit it when
false. Lets a minimal custom harness drop release(), matching the README example.
registerAction now registers the action descriptor (name + JSON-Schema input +
availableTo + handler_agent) on the relay and subscribes the handler agent to
its action.invoked events: on invocation it loads the input via getInvocation,
runs the local ActionRegistry (which still validates, enforces availableTo, and
executes the handler), and posts the result back with completeInvocation. The
agent-relay MCP now invokes actions through the relay and returns the invocation
ack instead of running handlers inline.

When no agent-scoped relay connection is present the action stays purely
in-process (unchanged legacy behavior), so existing callers and tests are
unaffected. action.invoked is routed only to the handler agent's WS connection
(confirmed against the relaycast engine), so relay wiring activates only for
agent-scoped clients.
Dedicated relay.webhooks surface over the relaycast 2.3.0 endpoints:
- createInbound({ channel }) -> { url, token, webhookId, channel }; external
  services POST { message, author } with the bearer token to post into a channel
- subscribe({ url, events, secret, headers }) for outbound, HMAC-signed delivery
- plus list/delete (inbound) and subscriptions/unsubscribe (outbound)

Additive: relay.integrations is unchanged so the CLI keeps working.
Document the v8 SDK redesign in CHANGELOG [Unreleased] (Added/Changed/Breaking
Changes/Migration Guidance): live-client register/reconnect, addListener, the
relay.webhooks namespace, fire-and-forget actions, createHuman, messageId, and
the removals of as()/relay.on/relay.notify/relay.sendMessage/relay.actions.

Add tests/integration/sdk/v8-api-smoke.mjs — a live-relay smoke script for the
new surface (register/reconnect, channels, addListener + envelope, sendMessage
routing, reply/react/messageId, inbound webhook). Requires a real workspace; not
part of the vitest suite.
Update web/content/docs/*.mdx to the redesigned surface: register/reconnect
returning live clients, agent-scoped sendMessage/reply/react with messageId and
to-sigil routing, addListener (dotted names/wildcards/predicates) with the
discriminated event + envelope, fire-and-forget registerAction, relay.webhooks,
createHuman, and defineHarness with optional release. Rewrite migration.mdx as a
from-old/to-v8 guide. Refresh the SDK rules facade example to match.
@willwashburn willwashburn requested a review from khaliqgant as a code owner June 3, 2026 06:04
@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 3, 2026

CodeAnt AI is reviewing your PR.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Warning

Review limit reached

@willwashburn, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 53 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d30ed73b-f250-4828-a8cd-b30e8b248bbe

📥 Commits

Reviewing files that changed from the base of the PR and between 226250a and 5523c9a.

📒 Files selected for processing (5)
  • packages/sdk/src/__tests__/listeners.test.ts
  • packages/sdk/src/__tests__/register-action-relay.test.ts
  • packages/sdk/src/actions/json-schema-lite.ts
  • packages/sdk/src/facade.ts
  • packages/sdk/src/listeners.ts
📝 Walkthrough

Walkthrough

Refactors SDK to v8 live-client model with unified addListener events, relay-wired actions, and webhooks. Updates CLI, harnesses, OpenClaw, tests, and extensive docs. Adds messageId, agents.me, action invocation events, inbound/outbound webhooks, and trajectory/trace artifacts.

Changes

SDK v8 API and webhooks end-to-end

Layer / File(s) Summary
Trajectory and trace artifacts
.agentworkforce/trajectories/...
Adds completed trajectory summaries, JSON records, and trace manifests for the SDK/docs/webhooks work.
Public contracts, event mapping, and lifecycle flags
packages/sdk/src/listeners.ts, packages/sdk/src/messaging/{types,normalize}.ts, packages/sdk/src/session/types.ts
Introduces RelayEvent envelope and addListener selector matching; adds messageId, action-invocation events/contracts; makes AgentSession.release optional; maps normalize() to new shapes.
Relaycast client: action lifecycle and webhooks
packages/sdk/src/actions/json-schema-lite.ts, packages/sdk/src/messaging/relaycast.ts
Converts action schemas to JSON Schema; adds agents.me, action invoke/get/complete, inbound webhooks and subscriptions with normalization.
Facade and AgentRelay live client assembly
packages/sdk/src/{facade.ts,agent-relay.ts}
Workspace register/reconnect return live clients; adds group DMs; wires actions via relay; exposes webhooks/capabilities and addListener.
CLI and harness consumer updates
packages/cli/..., packages/harnesses/...
MCP tools invoke via relay with local fallback; construct AgentRelay with agentToken; add createHuman and re-exports; update examples to ActionRegistry/live clients.
OpenClaw unified reaction events
packages/openclaw/...
Switches reactionAdded/Removed to unified messageReacted across gateway and tests; updates dependency.
SDK tests and integration coverage
packages/sdk/src/__tests__/*, tests/integration/sdk/v8-api-smoke.mjs, packages/sdk/package.json
Expands tests for live clients, addListener, action relay wiring, webhooks; adds integration smoke script; updates test script and dependency.
README, changelog, and top-level guidance
README.md, CHANGELOG.md, .claude/rules/sdk.md
Rewrites quickstart and guidance to v8 model; documents breaking/migration notes and new APIs.
Documentation pages and navigation
web/content/docs/*, web/lib/docs-nav.ts
Broad docs rewrite to v8 (events, messaging, actions, webhooks, harnesses, workspaces, migration); adds Events nav entry.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AgentRelay
  participant Relaycast
  participant WebhookEndpoint
  Client->>AgentRelay: registerAction(name, input schema, handler)
  AgentRelay->>Relaycast: commands.available/register
  Client->>Relaycast: invoke action (fire-and-forget)
  Relaycast-->>AgentRelay: actionInvoked event
  AgentRelay->>Relaycast: completeInvocation(output/error)
  Relaycast-->>WebhookEndpoint: HMAC event delivery (action.completed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • relaycast#158 — Adds relay.webhooks (inbound/subscribe) and agents.me, matching the issue’s webhook and agent-token/reconnect requirements.

Suggested reviewers

  • khaliqgant
  • barryollama

Poem

A rabbit taps keys with delighted thumps,
Wiring webhooks through relay jumps.
Actions now fly, then softly land—
Completed events wave a tiny hand.
Listeners bloom like clover bright,
Live clients hop into the light.
v8 burrows—everything feels right.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/upbeat-tesla-zqeKw

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Jun 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: 2ad2183773

ℹ️ 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 thread packages/sdk/src/facade.ts Outdated
});

// Subscribe to invocations routed to this handler agent.
return wiring.messaging.events.on('actionInvoked', async (event) => {
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 Start the event stream when wiring relay actions

When an agent-scoped Relaycast client registers an action and another agent invokes it, this only attaches an actionInvoked handler but never starts the event stream. In RelaycastMessagingClient, events.on(...) just stores handlers; the socket is opened only by events.connect(), which calls agent.connect() and forwards Relaycast events into this emitter. Without an explicit manual connect step, the handler loop never sees invocations and never calls completeInvocation, so relay-routed fire-and-forget actions remain stuck after the ack.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Preview deployed!

Environment URL
Web https://ddzxmrteo83f1.cloudfront.net

This preview will be cleaned up when the PR is merged or closed.

Comment on lines +421 to +434
context.events.on('any', (raw) => {
const evt = toPublicMessagingEvent(raw);
if (evt && matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
context.onActionEvent((raw) => {
const evt = toPublicActionEvent(raw);
if (matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
context.onSessionEvent(({ agentId, event }) => {
const evt = toPublicSessionEvent(agentId, event);
if (matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
];
return () => offs.forEach((off) => off());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Architect Review — HIGH

relay.addListener('message.*' | '*') never establishes a relay event connection, and in the common AgentRelay.createWorkspace() usage the underlying RelaycastMessagingClient.events cannot connect without an agent-scoped client. As a result, message events will not reach workspace-level listeners even though the v8 docs/quickstart show relay.addListener('message.created', ...) as the primary path.

Suggestion: Change addListener (or the AgentRelay facade) so that subscriptions to message events are backed by a real agent-scoped event source—e.g. implicitly call events.connect/subscribe on an agent client, or throw clearly when no agent-scoped connection exists—instead of silently registering handlers against an event surface that can never receive message events.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.

**Path:** packages/sdk/src/listeners.ts
**Line:** 421:434
**Comment:**
	*HIGH: `relay.addListener('message.*' | '*')` never establishes a relay event connection, and in the common `AgentRelay.createWorkspace()` usage the underlying `RelaycastMessagingClient.events` cannot connect without an agent-scoped client. As a result, message events will not reach workspace-level listeners even though the v8 docs/quickstart show `relay.addListener('message.created', ...)` as the primary path.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix

Comment on lines +605 to +612
function asInputRecord(input: unknown): Record<string, unknown> | undefined {
if (input === undefined || input === null) {
return undefined;
}
if (typeof input === 'object' && !Array.isArray(input)) {
return input as Record<string, unknown>;
}
return { input };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The relay invocation path rewrites non-object inputs (string, number, boolean) to { input: ... } and drops null entirely, which changes the action payload shape compared to local invocation. Actions whose schemas expect primitive or null input will start failing validation (or receive wrong data) when routed through relay fire-and-forget. Keep payload semantics consistent between local and relay paths (either enforce object-only inputs up front or serialize/deserialize primitives explicitly without shape mutation). [api mismatch]

Severity Level: Major ⚠️
- ❌ Primitive-input actions via MCP fail when relay enabled.
- ❌ Null-typed action inputs mis-validated when invoked through relay.
- ⚠️ Local vs relay invocation behave inconsistently for same action.
- ⚠️ Harder to reason about action schemas across environments.
Steps of Reproduction ✅
1. In an app using this repo, construct an `AgentRelayActions` implementation (e.g. `new
ActionRegistry()` from `packages/sdk/src/actions/registry.ts:19-24`) and register an
action whose input schema expects a primitive, such as `z.string()`, via
`registerFacadeAction` in `packages/sdk/src/facade.ts:190-205` (pass `inputSchema:
z.string()` so the handler receives a plain string).

2. Pass this `actions` instance into the CLI MCP server by calling
`createAgentRelayMcpServer({ actions, ... })` in
`packages/cli/src/cli/agent-relay-mcp.ts:1651-1680`; this wires the registry into
`registerAgentRelayActionTools` at `packages/cli/src/cli/agent-relay-mcp.ts:486-523`.

3. Ensure the MCP server has an agent token so `getAgentClient` is valid (constructed at
`packages/cli/src/cli/agent-relay-mcp.ts:1772-79`) and the underlying `AgentClient` from
`@relaycast/sdk` exposes an `actions` surface; then invoke the MCP tool `invoke_action`
(registered at `packages/cli/src/cli/agent-relay-mcp.ts:546-105`) with payload `{ "name":
"echo_primitive", "input": "hello" }`.

4. The tool handler calls `invokeAction(name, input)` in `registerAgentRelayActionTools`
(`agent-relay-mcp.ts:502-64`), which sees `relayActions` present and executes
`relayActions.invoke(name, asInputRecord(input))`; `asInputRecord` at
`agent-relay-mcp.ts:605-612` turns the string `"hello"` into `{ input: "hello" }`, so the
Relay service stores a record-shaped `invocation.input`.

5. On the handler side, when the relay emits `actionInvoked`, `handleActionInvoked` in
`packages/sdk/src/facade.ts:430-69` loads the invocation via `commands.getInvocation` and
sets `input = invocation.input ?? {}`, so `input` becomes `{ input: "hello" }`, then calls
`actions.invoke({ name, input, caller })`.

6. `InMemoryAgentRelayActions.invoke` in `packages/sdk/src/actions/registry.ts:65-106`
validates this `input` against the registered `z.string()` schema via
`validateActionSchema(input.input, definition.inputSchema)`; because the schema expects a
string but receives an object `{ input: "hello" }`, validation fails and the action
returns `{ ok: false, error: { code: 'invalid_input', ... } }` without ever calling the
handler.

7. If the same MCP tool is run in an environment where
`getRelayAgentActions(getAgentClient)` returns `undefined` (no relay action surface),
`invokeAction` falls back to `actions.invoke({ name, input, context })` with `input` kept
as the raw `"hello"` string (see `agent-relay-mcp.ts:514-522`), so the exact same action
and tool behave differently depending on relay availability.

8. A similar mismatch occurs for `null` input: `asInputRecord(null)` returns `undefined`,
so the relay stores no input, `handleActionInvoked` coerces this to `{}`, and a schema
like `z.null()` or `{ type: 'null' }` in `actions/registry.ts` will fail validation
remotely while potentially succeeding when invoked locally via the fallback path.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/cli/src/cli/agent-relay-mcp.ts
**Line:** 605:612
**Comment:**
	*Api Mismatch: The relay invocation path rewrites non-object inputs (`string`, `number`, `boolean`) to `{ input: ... }` and drops `null` entirely, which changes the action payload shape compared to local invocation. Actions whose schemas expect primitive or null input will start failing validation (or receive wrong data) when routed through relay fire-and-forget. Keep payload semantics consistent between local and relay paths (either enforce object-only inputs up front or serialize/deserialize primitives explicitly without shape mutation).

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

this.relayAgentClient.on.reactionRemoved((event: ReactionRemovedEvent) => {
this.relayAgentClient.on.messageReacted((event: MessageReactedEvent) => {
if (!this.shouldProcessWsInbound()) return;
const action = event.action ?? 'added';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Defaulting missing reaction action to added can misclassify removal events as additions. If the upstream event payload omits action (or uses type-only signaling), removed reactions will be emitted and logged as added, producing incorrect inbound notifications. Derive the action from a reliable event discriminator (or ignore events without explicit action) instead of forcing an added default. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ OpenClaw notifications misreport some reaction removals as additions.
- ⚠️ Gateway logs show incorrect direction for affected reaction events.
Steps of Reproduction ✅
1. Start an `InboundGateway` instance and call `gateway.start()` as in
`packages/openclaw/src/__tests__/gateway-control.test.ts:13-22`, which wires Relaycast
events via `bindRelayAgentHandlers()` in `packages/openclaw/src/gateway.ts:1360-1399`.

2. A Relaycast WebSocket reaction event is delivered to the inbound gateway and passed to
the SDK's `on.messageReacted` handler, invoking the callback at
`packages/openclaw/src/gateway.ts:1396-1402` with a `MessageReactedEvent` whose `action`
field is absent or undefined for a removal.

3. The handler executes `const action = event.action ?? 'added';` at
`packages/openclaw/src/gateway.ts:1398`, coercing the missing `action` to the string
`"added"` and logging it as an addition at `packages/openclaw/src/gateway.ts:1399-1401`.

4. The mislabelled `action` is then propagated into `handleRealtimeReaction(event,
action)` at `packages/openclaw/src/gateway.ts:1402` and used to build the synthetic
inbound message text at `packages/openclaw/src/gateway.ts:2113-2128`, causing removal
events with no `action` field to be surfaced to OpenClaw as added reactions.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/openclaw/src/gateway.ts
**Line:** 1398:1398
**Comment:**
	*Api Mismatch: Defaulting missing reaction action to `added` can misclassify removal events as additions. If the upstream event payload omits `action` (or uses type-only signaling), removed reactions will be emitted and logged as added, producing incorrect inbound notifications. Derive the action from a reliable event discriminator (or ignore events without explicit action) instead of forcing an `added` default.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@@ -1,4 +1,7 @@
import { createRequire } from 'node:module';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Importing node:module at module top-level hard-couples this SDK path to Node runtimes and will fail in browser/edge environments before any fallback logic runs. Since this file is part of the published SDK surface, gate Node-specific loading behind a lazy/dynamic strategy that does not require resolving node: modules at import time. [api mismatch]

Severity Level: Major ⚠️
- ❌ Browser and edge builds importing actions fail to bundle.
- ⚠️ Prevents using SDK actions in web/worker runtimes.
Steps of Reproduction ✅
1. A browser or edge application bundles `@agent-relay/sdk` to use actions (for example,
importing `ActionRegistry` or `actionSchemaToJsonSchema` via the public re-export in
`packages/sdk/src/actions/index.ts:1-4`).

2. The bundler resolves `packages/sdk/dist/actions/json-schema-lite.js` (built from
`packages/sdk/src/actions/json-schema-lite.ts:1-121`), which contains the top-level
`import { createRequire } from 'node:module';` statement at line 1.

3. Because `node:module` is a Node-only built-in and this package does not declare a
browser-specific export in `packages/sdk/package.json:1-38`, typical browser/edge bundlers
or runtimes fail to resolve the `node:` specifier, causing a build-time resolution error
or runtime `Module not found`/`Cannot resolve 'node:module'` error before any of the lazy
`tryZodToJsonSchema(...)` fallback logic at
`packages/sdk/src/actions/json-schema-lite.ts:91-120` can run.

4. As a result, consumers cannot use the SDK's actions surface (including `ActionRegistry`
and schema helpers) in non-Node environments even when they never rely on Zod conversion,
because the Node-specific `node:module` import is evaluated at module load.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/actions/json-schema-lite.ts
**Line:** 1:1
**Comment:**
	*Api Mismatch: Importing `node:module` at module top-level hard-couples this SDK path to Node runtimes and will fail in browser/edge environments before any fallback logic runs. Since this file is part of the published SDK surface, gate Node-specific loading behind a lazy/dynamic strategy that does not require resolving `node:` modules at import time.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

return zodSchema;
}

return { type: 'object', additionalProperties: true };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Falling back to a fully permissive schema when validator conversion fails silently broadens the registered relay descriptor and breaks schema contract consistency. Callers will see/consume an object-accepts-anything schema even though local validation may reject many inputs, causing avoidable invocation failures and integration drift. Preserve failure visibility (or avoid registering schema) rather than advertising an overly broad schema. [logic error]

Severity Level: Critical 🚨
- ❌ Relay action descriptors misrepresent validator constraints to remote callers.
- ❌ Remote invocations see frequent validation failures despite permissive schemas.
- ⚠️ MCP/tooling UIs generate overly broad forms for such actions.
Steps of Reproduction ✅
1. A caller registers an action via the high-level facade `AgentRelay.registerAction(...)`
(entry point `packages/sdk/src/agent-relay.ts:193-199`), passing a non-Zod validator that
implements `safeParse` and matches `SafeParseSchema` from
`packages/sdk/src/actions/types.ts:64-66` as `inputSchema`/`outputSchema`.

2. Local runtime validation for invocations uses `validateActionSchema(...)` in
`packages/sdk/src/actions/registry.ts:258-271`, where `isZodLikeSchema(...)` at
`packages/sdk/src/actions/registry.ts:23-29` recognizes any object with `safeParse` and
calls `schema.safeParse(input)` for both inputs and outputs.

3. When the same action is wired to the relay from an agent-scoped client,
`wireRelayAction(...)` in `packages/sdk/src/facade.ts:50-75` calls
`actionSchemaToJsonSchema(def.inputSchema ?? def.input)` and
`actionSchemaToJsonSchema(def.outputSchema ?? def.output)` to build the descriptor sent
through `RelayMessaging.commands.register`.

4. In `actionSchemaToJsonSchema(...)`
(`packages/sdk/src/actions/json-schema-lite.ts:338-366`), the validator is recognized by
`isValidatorSchema(...)` at line 49, passed to `validatorToJsonSchema(...)` at lines
64-88, and then through `tryZodToJsonSchema(...)` at lines 91-120; because the validator
is not a real Zod schema (no `_zod`/`_def` markers), `tryZodToJsonSchema` returns
`undefined`, so `validatorToJsonSchema` executes `return { type: 'object',
additionalProperties: true };` at line 407, registering a fully permissive object schema
on the relay even though the local `safeParse` rejects many shapes, leading to remote
callers seeing an allow-all schema while invocations often fail with
`invalid_input`/`invalid_output`.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/actions/json-schema-lite.ts
**Line:** 407:407
**Comment:**
	*Logic Error: Falling back to a fully permissive schema when validator conversion fails silently broadens the registered relay descriptor and breaks schema contract consistency. Callers will see/consume an `object`-accepts-anything schema even though local validation may reject many inputs, causing avoidable invocation failures and integration drift. Preserve failure visibility (or avoid registering schema) rather than advertising an overly broad schema.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +297 to +309
const list = Array.isArray(agents) ? agents : [agents];
const clients: RelayAgentClient[] = [];
for (const agent of list) {
const input =
typeof agent === 'string'
? { name: stripSigil(agent) }
: {
name: resolveAgentName(agent),
type: agent.type,
persona: agent.persona,
metadata: agent.metadata,
};
clients.push(deps.buildAgentClient(await messaging.agents.register(input)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Batch registration does not reject duplicate agent names before making network calls. If duplicates are passed, registration can partially succeed (earlier agents created, later call fails), leaving inconsistent state and violating the advertised duplicate-rejection behavior. Pre-validate uniqueness and fail fast before registering any agent. [logic error]

Severity Level: Major ⚠️
- ⚠️ Batch workspace.register can partially succeed on duplicate names.
- ⚠️ Workspace may contain agents despite overall registration failure.
Steps of Reproduction ✅
1. `AgentRelay.workspace` in `packages/sdk/src/agent-relay.ts:163-171` constructs a
`RelayWorkspace` via `createWorkspaceFacade(this.messaging, { buildAgentClient,
reconnectAgent })`, so all workspace registrations use `createWorkspaceFacade` from
`packages/sdk/src/facade.ts:289-324`.

2. The `register` implementation in `createWorkspaceFacade`
(`packages/sdk/src/facade.ts:293-312`) normalizes the input into `list =
Array.isArray(agents) ? agents : [agents];` and then loops, building an `input` with
`name: stripSigil(agent)` or `resolveAgentName(agent)` and calling
`messaging.agents.register(input)` for each element, pushing a corresponding
`RelayAgentClient` into `clients`.

3. There is no pre-validation for duplicate names in `list`, so if a caller does `await
relay.workspace.register(['alice', 'alice']);` (or two `AgentRef` objects that resolve to
the same name), the first `messaging.agents.register` call succeeds and creates an agent
named `alice`, the second call likely fails with a backend duplicate-name error, and the
overall `register` promise rejects.

4. At this point, the workspace already has a newly-created `alice` agent and a live
`RelayAgentClient` was created for it, but the caller only sees a rejected promise and
cannot distinguish this partial success, violating the documented "duplicate names are
rejected" behavior and leaving inconsistent state when batch registration inputs
accidentally contain duplicates.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/facade.ts
**Line:** 297:309
**Comment:**
	*Logic Error: Batch registration does not reject duplicate agent names before making network calls. If duplicates are passed, registration can partially succeed (earlier agents created, later call fails), leaving inconsistent state and violating the advertised duplicate-rejection behavior. Pre-validate uniqueness and fail fast before registering any agent.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +375 to +380
return {
unregister: () => {
relayUnsubscribe();
localHandle.unregister();
},
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Unregistering a relay-wired action only removes local listener/registry state and leaves the relay descriptor registered remotely. That creates stale server-side actions that can still be invoked and drift from local behavior. Include remote descriptor cleanup (e.g., delete command) during unregister. [missing cleanup]

Severity Level: Major ⚠️
- ⚠️ Relay continues advertising actions with no active handler.
- ❌ Post-unregister invocations can hang or fail without completion.
Steps of Reproduction ✅
1. A live agent client is assembled via `assembleAgentClient` in
`packages/sdk/src/agent-relay.ts:281-299`, which calls `agentRelayAgent(messaging,
actions, identity.name)` and exposes `registerAction` that delegates to
`registerFacadeAction(actions, def, { messaging, handlerAgent })` at
`packages/sdk/src/agent-relay.ts:251-274`.

2. In `registerFacadeAction` (`packages/sdk/src/facade.ts:338-381`), `wireRelayAction` is
called, which invokes `commands.register(...)` at `packages/sdk/src/facade.ts:8-16` to
register a command descriptor on the relay for this action name and `handlerAgent`.

3. The same `registerFacadeAction` then constructs an `ActionHandle` whose `unregister`
implementation (snippet at `packages/sdk/src/facade.ts:375-380`) only calls
`relayUnsubscribe()`—which removes the local `events.on('actionInvoked', ...)` handler—and
`localHandle.unregister()`, which removes the handler from the in-process
`ActionRegistry`.

4. There is no corresponding `commands.delete(...)` or other remote cleanup call anywhere
in the codebase (verified by the absence of `commands.delete` usages in
`packages/sdk/src`), so after `unregister()` is called the relay continues to advertise
the capability and route invocations to the `handlerAgent`, but this process no longer
listens or completes them, leaving stale server-side state and potentially hanging
invocations.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/facade.ts
**Line:** 375:380
**Comment:**
	*Missing Cleanup: Unregistering a relay-wired action only removes local listener/registry state and leaves the relay descriptor registered remotely. That creates stale server-side actions that can still be invoked and drift from local behavior. Include remote descriptor cleanup (e.g., delete command) during unregister.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +420 to +434
const offs = [
context.events.on('any', (raw) => {
const evt = toPublicMessagingEvent(raw);
if (evt && matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
context.onActionEvent((raw) => {
const evt = toPublicActionEvent(raw);
if (matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
context.onSessionEvent(({ agentId, event }) => {
const evt = toPublicSessionEvent(agentId, event);
if (matchesSelector(selector, evt.type)) runHandler(handler, evt);
}),
];
return () => offs.forEach((off) => off());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: addListener only attaches handlers via events.on(...) but never establishes the underlying event stream connection. With RelaycastMessagingClient, listeners won't receive any runtime events unless events.connect() is called first, so addListener(...) appears to silently do nothing in normal usage. Ensure the listener path starts the event stream (idempotently) when the first listener is added. [incomplete implementation]

Severity Level: Critical 🚨
- ❌ relay.addListener handlers never fire without manual events.connect.
- ⚠️ Real-time message/status/events features effectively disabled by default.
Steps of Reproduction ✅
1. `AgentRelay` constructs its listener hub lazily in the `listenerHub` getter at
`packages/sdk/src/agent-relay.ts:140-145`, which calls
`createListenerHub(this.messaging.events, this.actions)` and exposes
`addListener(selector, handler)` by delegating to `this.listenerHub.addListener` at
`packages/sdk/src/agent-relay.ts:201-204`.

2. `createListenerHub` in `packages/sdk/src/listeners.ts:397-453` builds a
`ListenerContext` with `events: baseEvents` (the `RelayMessagingEventsSurface` returned by
`RelaycastMessagingClient.events`) and defines `addListener`, which for string selectors
subscribes via `context.events.on('any', ...)`, `context.onActionEvent(...)`, and
`context.onSessionEvent(...)` (snippet at `packages/sdk/src/listeners.ts:420-433`), but
never calls `baseEvents.connect()` on the messaging client.

3. For the default `RelaycastMessagingClient`, its `events` surface only starts streaming
WebSocket events to `events.on(...)` subscribers when `events.connect()` is invoked; this
is implemented in `packages/sdk/src/messaging/relaycast.ts:30-37`, where
`events.connect()` calls `agent.connect()` and wires `agent.on.any(...)` into
`emitEvent(normalizeMessagingEvent(event))`.

4. There are no production calls to `messaging.events.connect()` in the `AgentRelay` /
`listeners.ts` path (the only explicit `client.events.connect()` usage is in tests at
`packages/sdk/src/__tests__/messaging.test.ts:343-347`), so a consumer who uses the new
public API `relay.addListener('message.*', handler)` on an `AgentRelay` backed by
`RelaycastMessagingClient` will register handlers that never receive events unless they
separately know to call `relay.messaging.events.connect()`, making `addListener` appear to
silently do nothing in normal usage.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/listeners.ts
**Line:** 420:434
**Comment:**
	*Incomplete Implementation: `addListener` only attaches handlers via `events.on(...)` but never establishes the underlying event stream connection. With `RelaycastMessagingClient`, listeners won't receive any runtime events unless `events.connect()` is called first, so `addListener(...)` appears to silently do nothing in normal usage. Ensure the listener path starts the event stream (idempotently) when the first listener is added.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

} from './actions/index.js';
import type { DeliveryMode } from './delivery/index.js';
import type { RelayAgentHandle } from './listeners.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The invocation filter only checks action name and ignores handlerAgentId, even though the event includes it. If multiple handlers share an action name, this client can execute and complete invocations that were routed for another handler agent. Add a handler-agent match check before executing. [incorrect condition logic]

Severity Level: Critical 🚨
- ❌ Multiple agents can execute the same relay action invocation.
- ❌ Wrong agent may complete invocation, violating caller expectations.
- ⚠️ Races and inconsistent side effects in action handlers.
Steps of Reproduction ✅
1. The Relaycast backend emits `action.invoked` WebSocket events that are normalized into
`RelayActionInvokedEvent` objects with `type`, `invocationId`, `actionName`, `callerName`,
and `handlerAgentId` in `packages/sdk/src/messaging/normalize.ts:16-23` and typed in
`packages/sdk/src/messaging/types.ts:25-31`.

2. `wireRelayAction` in `packages/sdk/src/facade.ts:389-427` subscribes to these with
`wiring.messaging.events.on('actionInvoked', async (event) => { ... })` (snippet lines
21-27), but only checks `if (event.actionName !== def.name) return;` before calling
`handleActionInvoked(actions, commands, def.name, event);`—it does not inspect
`event.handlerAgentId` at all.

3. If two distinct handler agents, e.g. `agentA` and `agentB`, both call
`agentRelayAgent(messagingForToken(tokenA), actions, 'agentA')` and
`agentRelayAgent(messagingForToken(tokenB), actions, 'agentB')` in
`packages/sdk/src/agent-relay.ts:251-279` and each registers the same action name (such as
`'deploy'`) via `registerAction`, then each call to `registerFacadeAction` will set up its
own `actionInvoked` listener using the same shared `RelaycastMessagingClient` event
stream.

4. When the relay emits an `actionInvoked` event for `actionName: 'deploy'` targeted at
`handlerAgentId: 'agentB'`, both listeners see `event.actionName === def.name` and will
invoke `handleActionInvoked`, causing both processes to load the same invocation, run
their local handlers, and call `commands.completeInvocation(...)`—so invocations routed to
`agentB` may be executed and completed by `agentA` as well, leading to double execution
and incorrect handler attribution since there is no `handlerAgentId` filter.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/sdk/src/facade.ts
**Line:** 23:26
**Comment:**
	*Incorrect Condition Logic: The invocation filter only checks action name and ignores `handlerAgentId`, even though the event includes it. If multiple handlers share an action name, this client can execute and complete invocations that were routed for another handler agent. Add a handler-agent match check before executing.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 3, 2026

CodeAnt AI finished reviewing your PR.

@agent-relay-code
Copy link
Copy Markdown
Contributor

Fixed one PR issue found during review:

  • packages/openclaw/src/gateway.ts now handles relaycast 2.x message.reacted events in poll fallback replay, matching the WebSocket path.
  • Added regression coverage in packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts.

Verified locally:

  • npm --prefix packages/sdk run check passed.
  • Targeted SDK/OpenClaw Vitest run passed: 13 files, 137 tests.
  • packages/cli build was not usable in this checkout because local workspace dependency resolution is broken/stale, resolving missing workspace packages and old global @relaycast/sdk types.

@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Reviewed and fixed PR #1029 fallout.

Changed:

  • Updated packages/harnesses/src/readme-quickstart.test.ts off removed relay.on / relay.notify surfaces.
  • Updated SDK README examples off removed relay.as / relay.actions.
  • Widened MCP relaycast agent client typing for relaycast 2.x .actions.
  • Cleaned stale internal comments referencing relay.on.

Verified:

  • tsc -p packages/sdk/tsconfig.json --noEmit
  • tsc -p packages/sdk/tsconfig.build.json
  • node --check on modified TS files

Could not run full workspace CI locally because npm ci was killed by the environment with exit 137, leaving dependency installs incomplete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

8 issues found across 64 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/cli/agent-relay-mcp.ts">

<violation number="1" location="packages/cli/src/cli/agent-relay-mcp.ts:600">
P1: Catch-all here hides real client errors. It silently falls back to local invoke. Surface the error instead.</violation>

<violation number="2" location="packages/cli/src/cli/agent-relay-mcp.ts:606">
P2: Null input gets erased in relay invoke path. `null` turns into missing payload. Keep `null` as data.</violation>
</file>

<file name="web/content/docs/delivery.mdx">

<violation number="1" location="web/content/docs/delivery.mdx:178">
P1: `event.messageId` does not exist on the `delivery.failed` event type. The type defined in this same doc only has `deliveryId`, `agent`, `reason`, `retryable`. Using `event.messageId` here produces `undefined` at runtime and would show 'Delivery ... failed for undefined: ...'. Either add `messageId` to the `delivery.failed` event variant, or remove the reference and use `event.deliveryId` alone.</violation>
</file>

<file name="web/content/docs/session-capabilities.mdx">

<violation number="1" location="web/content/docs/session-capabilities.mdx:17">
P2: Messaging Capabilities section type example still shows `attachments:` without `?`, inconsistent with the updated top-level type that now makes it optional.

Reader sees two different type shapes for the same field.</violation>
</file>

<file name="packages/sdk/src/actions/json-schema-lite.ts">

<violation number="1" location="packages/sdk/src/actions/json-schema-lite.ts:351">
P2: Boolean `false` schema gets dropped. This turns “reject all” into “no constraint” when sending action descriptors. Preserve false-schema semantics in JSON object form.</violation>
</file>

<file name="packages/sdk/src/listeners.ts">

<violation number="1" location="packages/sdk/src/listeners.ts:287">
P2: Discriminated union not discriminated. `RelaySessionEvent.type` uses `string`, so `RelayEvent` narrowing by `type` breaks. Make this a specific union type.</violation>

<violation number="2" location="packages/sdk/src/listeners.ts:418">
P2: Predicate path skips public event mapping. `addListener(predicate, handler)` can receive raw legacy events, not `RelayEvent`. This breaks the new API contract and can surprise handlers.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:182">
P2: Missing `z` import in action examples that use Zod schemas. Add `import { z } from 'zod'` at the top of the code block, or add a comment showing the required import if the surrounding examples don't need it.</violation>
</file>

Note: This PR contains a large number of files. Free-tier reviews are limited to 40 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review. Paid plans can review more files.

Re-trigger cubic

Comment on lines +600 to +602
} catch {
return undefined;
}
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: Catch-all here hides real client errors. It silently falls back to local invoke. Surface the error instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/agent-relay-mcp.ts, line 600:

<comment>Catch-all here hides real client errors. It silently falls back to local invoke. Surface the error instead.</comment>

<file context>
@@ -562,24 +581,41 @@ function registerAgentRelayActionTools(
+  }
+  try {
+    return getAgentClient().actions;
+  } catch {
+    return undefined;
+  }
</file context>
Suggested change
} catch {
return undefined;
}
} catch (error) {
throw error;
}

text: `${engineer.identity.handle} could not receive ${event.message.id}: ${event.reason}`,
});
});
text: `Delivery ${event.deliveryId} failed for ${event.messageId}: ${event.reason}.`,
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: event.messageId does not exist on the delivery.failed event type. The type defined in this same doc only has deliveryId, agent, reason, retryable. Using event.messageId here produces undefined at runtime and would show 'Delivery ... failed for undefined: ...'. Either add messageId to the delivery.failed event variant, or remove the reference and use event.deliveryId alone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/content/docs/delivery.mdx, line 178:

<comment>`event.messageId` does not exist on the `delivery.failed` event type. The type defined in this same doc only has `deliveryId`, `agent`, `reason`, `retryable`. Using `event.messageId` here produces `undefined` at runtime and would show 'Delivery ... failed for undefined: ...'. Either add `messageId` to the `delivery.failed` event variant, or remove the reference and use `event.deliveryId` alone.</comment>

<file context>
@@ -172,12 +172,12 @@ type DeliveryEvent =
-    text: `${engineer.identity.handle} could not receive ${event.message.id}: ${event.reason}`,
-  });
-});
+    text: `Delivery ${event.deliveryId} failed for ${event.messageId}: ${event.reason}.`,
+  })
+);
</file context>
Suggested change
text: `Delivery ${event.deliveryId} failed for ${event.messageId}: ${event.reason}.`,
text: `Delivery ${event.deliveryId} failed: ${event.reason}.`,

}

function asInputRecord(input: unknown): Record<string, unknown> | undefined {
if (input === undefined || input === null) {
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: Null input gets erased in relay invoke path. null turns into missing payload. Keep null as data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/agent-relay-mcp.ts, line 606:

<comment>Null input gets erased in relay invoke path. `null` turns into missing payload. Keep `null` as data.</comment>

<file context>
@@ -562,24 +581,41 @@ function registerAgentRelayActionTools(
+}
+
+function asInputRecord(input: unknown): Record<string, unknown> | undefined {
+  if (input === undefined || input === null) {
+    return undefined;
+  }
</file context>

receive: true;
send?: boolean;
attachments: Array<'text' | 'image'>;
attachments?: Array<'text' | 'image'>;
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: Messaging Capabilities section type example still shows attachments: without ?, inconsistent with the updated top-level type that now makes it optional.

Reader sees two different type shapes for the same field.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/content/docs/session-capabilities.mdx, line 17:

<comment>Messaging Capabilities section type example still shows `attachments:` without `?`, inconsistent with the updated top-level type that now makes it optional.

Reader sees two different type shapes for the same field.</comment>

<file context>
@@ -14,7 +14,7 @@ type AgentSessionCapabilities = {
     receive: true;
     send?: boolean;
-    attachments: Array<'text' | 'image'>;
+    attachments?: Array<'text' | 'image'>;
   };
 
</file context>

schema: ActionSchema | undefined
): Record<string, unknown> | undefined {
if (schema === undefined) {
return undefined;
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: Boolean false schema gets dropped. This turns “reject all” into “no constraint” when sending action descriptors. Preserve false-schema semantics in JSON object form.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/src/actions/json-schema-lite.ts, line 351:

<comment>Boolean `false` schema gets dropped. This turns “reject all” into “no constraint” when sending action descriptors. Preserve false-schema semantics in JSON object form.</comment>

<file context>
@@ -331,3 +334,105 @@ function jsonEqual(left: unknown, right: JsonValue): boolean {
+  schema: ActionSchema | undefined
+): Record<string, unknown> | undefined {
+  if (schema === undefined) {
+    return undefined;
+  }
+
</file context>

}

export interface RelaySessionEvent {
type: string;
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: Discriminated union not discriminated. RelaySessionEvent.type uses string, so RelayEvent narrowing by type breaks. Make this a specific union type.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/src/listeners.ts, line 287:

<comment>Discriminated union not discriminated. `RelaySessionEvent.type` uses `string`, so `RelayEvent` narrowing by `type` breaks. Make this a specific union type.</comment>

<file context>
@@ -221,6 +226,155 @@ export interface AgentHandleInput {
+}
+
+export interface RelaySessionEvent {
+  type: string;
+  agentId: string;
+  event: AgentSessionEvent;
</file context>
Suggested change
type: string;
type: AgentSessionEvent['type'];

handler: ListenerHandler<RelayEvent>
): (() => void) => {
if (typeof selector !== 'string') {
return selector.subscribe(context, handler as ListenerHandler);
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: Predicate path skips public event mapping. addListener(predicate, handler) can receive raw legacy events, not RelayEvent. This breaks the new API contract and can surprise handlers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/src/listeners.ts, line 418:

<comment>Predicate path skips public event mapping. `addListener(predicate, handler)` can receive raw legacy events, not `RelayEvent`. This breaks the new API contract and can surprise handlers.</comment>

<file context>
@@ -254,9 +410,34 @@ export function createListenerHub(
+    handler: ListenerHandler<RelayEvent>
+  ): (() => void) => {
+    if (typeof selector !== 'string') {
+      return selector.subscribe(context, handler as ListenerHandler);
+    }
+    const offs = [
</file context>

Comment thread README.md
```ts
relay.registerAction({
name: 'classify',
input: z.object({ foo: z.enum(['bar', 'bang']) }),
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: Missing z import in action examples that use Zod schemas. Add import { z } from 'zod' at the top of the code block, or add a comment showing the required import if the surrounding examples don't need it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 182:

<comment>Missing `z` import in action examples that use Zod schemas. Add `import { z } from 'zod'` at the top of the code block, or add a comment showing the required import if the surrounding examples don't need it.</comment>

<file context>
@@ -20,177 +20,198 @@ import { AgentRelay } from '@agent-relay/sdk';
+```ts
+relay.registerAction({
+  name: 'classify',
+  input: z.object({ foo: z.enum(['bar', 'bang']) }),
+  handler: async ({ input }) => ({ baz: input.foo }),
+  availableTo: [{ name: 'codex-1' }],
</file context>

- addListener and the fire-and-forget action wiring now call events.connect()
  (idempotent), so listeners and action handlers actually receive events; the
  socket was never opened before, leaving them silent after the ack. (Codex P1)
- json-schema-lite drops the static node:module import, resolving createRequire
  lazily via process.getBuiltinModule so browser/edge bundles don't break.
- A non-convertible action input schema now warns instead of silently
  advertising a permissive relay descriptor (local validation is unaffected).
- workspace.register() fails fast on in-batch duplicate names so a batch can't
  partially register before the relay rejects a later duplicate.

Add tests covering the event-stream connect on both addListener and registerAction.
@willwashburn
Copy link
Copy Markdown
Member Author

Thanks for the thorough automated reviews — went through each. Pushed fixes in 5523c9a7.

Fixed

  • Event stream never opened (Codex P1 · CodeAnt Critical, listeners.ts + facade.ts): the real one. addListener and the fire-and-forget action wiring called events.on(...) (which only registers handlers) but never events.connect() (which opens the socket), so listeners and action handlers stayed silent after the ack. Both now call events.connect() idempotently, with tests asserting it on both paths.
  • Static node:module import (CodeAnt Major, json-schema-lite.ts): dropped the top-level import; createRequire is now resolved lazily via process.getBuiltinModule('node:module') and returns undefined off Node, so browser/edge bundles don't fail to resolve it.
  • Silent permissive schema fallback (CodeAnt Critical, json-schema-lite.ts): a non-convertible action input schema now emits a console.warn instead of silently advertising a permissive relay descriptor. (Local validation always uses the real schema, so this only affects the relay-side descriptor.)
  • Batch register partial success on duplicates (CodeAnt Major, facade.ts): workspace.register([...]) now fails fast on in-batch duplicate names before any network call.

Acknowledged — by design or tracked follow-up

  • Workspace-level addListener can't stream (CodeAnt High, listeners.ts): correct, and it's a relaycast-SDK constraint — only agent-scoped clients expose connect()/on.* (the workspace client has no WS surface). Listeners on the live clients from register()/reconnect()/create() work; workspace-only relay.addListener registers handlers but has no stream. The connect call no-ops there rather than throwing. Backing workspace-level listeners with an agent stream is a follow-up; the recommended pattern meanwhile is to listen on an agent client.
  • action.invoked handler-agent match (CodeAnt Critical, facade.ts): relaycast fans action.invoked out only to the handler agent's own connection (verified in the engine: fanoutToAgents([handler_agent_id], ...)), and each agent client is a distinct WS connection, so a client can't receive another handler's invocation — the action-name filter is sufficient. Happy to add an explicit handlerAgentId guard as defense-in-depth if preferred.
  • MCP primitive/null input shaping (CodeAnt Major, agent-relay-mcp.ts): the relaycast actions.invoke API carries an object payload (Record<string, unknown>), so top-level primitive/null inputs aren't faithfully representable over the relay path — the documented convention is object input schemas (z.object(...)), which round-trip cleanly. This is a protocol constraint rather than a fixable shape bug.
  • unregister leaves the relay descriptor (CodeAnt Major, facade.ts): valid — follow-up to delete the remote descriptor on unregister (needs a delete on the agent-scoped commands surface).
  • OpenClaw reaction defaulting to added (CodeAnt Major, gateway.ts): both the WS message.reacted path and the poll-fallback path pass an explicit action; the ?? 'added' only applies if a payload omits it entirely. Low-risk; can tighten to ignore action-less events.

The headline functional bug (the unopened event stream) is fixed and tested. The remaining items are a relaycast-SDK streaming limitation, protocol constraints, and small hardening follow-ups, called out above.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (3)
packages/openclaw/src/gateway.ts (1)

1866-1879: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle unified message.reacted events in poll fallback too.

The realtime path now consumes messageReacted, but handlePolledEvent() still only recognizes reaction.added / reaction.removed. If the v8 poll envelope is already normalized to message.reacted, those events will hit the default branch and disappear whenever the gateway is in poll mode.

🔎 Verification script
#!/bin/bash
set -euo pipefail

echo "== Search for unified vs legacy reaction event names in the SDK/public contract =="
rg -n -C2 'message\.reacted|reaction\.added|reaction\.removed|messageReacted' \
  packages/sdk/src packages/openclaw/src

echo
echo "== Inspect the poll dispatcher in packages/openclaw/src/gateway.ts =="
sed -n '1835,1885p' packages/openclaw/src/gateway.ts
💡 Proposed fix
       case 'reaction.added':
         return (
           await this.handleRealtimeReaction(event.payload as unknown as MessageReactedEvent, 'added', {
             ...baseOptions,
             eventId: event.id,
           })
         ).committed;
       case 'reaction.removed':
         return (
           await this.handleRealtimeReaction(event.payload as unknown as MessageReactedEvent, 'removed', {
             ...baseOptions,
             eventId: event.id,
           })
         ).committed;
+      case 'message.reacted': {
+        const payload = event.payload as unknown as MessageReactedEvent;
+        const action = payload.action === 'removed' ? 'removed' : 'added';
+        return (
+          await this.handleRealtimeReaction(payload, action, {
+            ...baseOptions,
+            eventId: event.id,
+          })
+        ).committed;
+      }
       default:
         console.warn(`[gateway] Ignoring unknown polled event type: ${type || 'unknown'}`);
         return true;
🤖 Prompt for 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.

In `@packages/openclaw/src/gateway.ts` around lines 1866 - 1879, handlePolledEvent
currently only handles legacy 'reaction.added' / 'reaction.removed' names; add
support for the unified 'message.reacted' envelope by adding a case for
'message.reacted' in the same switch and call
handleRealtimeReaction(event.payload as MessageReactedEvent, action, {
...baseOptions, eventId: event.id }) returning .committed; determine the action
string ('added' or 'removed') from event.payload.action (or map payload fields
if the unified payload shape differs) so the logic in handleRealtimeReaction
continues to work for both legacy and v8-normalized events.
web/content/docs/reference-openclaw.mdx (1)

25-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make release() optional in the documented session contract.

This block still makes release() mandatory, but web/content/docs/session-capabilities.mdx now documents lifecycle.release: false with the method omitted. Leaving it required here gives adapter authors two conflicting public contracts.

Suggested doc fix
 type AgentSession = {
   identity: AgentIdentity;
   capabilities: AgentSessionCapabilities;
   receiveMessage(message: RelayMessage, ctx: MessageContext): Promise<MessageReceipt>;
   onEvent?(handler: (event: AgentSessionEvent) => void): Unsubscribe;
-  release(reason?: string): Promise<void>;
+  release?(reason?: string): Promise<void>;
 };
🤖 Prompt for 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.

In `@web/content/docs/reference-openclaw.mdx` around lines 25 - 32, The
AgentSession type currently requires release(), conflicting with
session-capabilities docs that document lifecycle.release: false; make release
optional on the documented contract by changing the AgentSession definition so
release?: Promise<void> or a method signature marked optional (i.e.,
release?(reason?: string): Promise<void>), and update any text around
AgentSession to reflect that adapters may omit the release method when
lifecycle.release is false; ensure references to AgentSession and release() in
this file are consistent with the optional contract.
web/content/docs/session-capabilities.mdx (1)

60-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the attachments field optional in the messaging snippet.

The top-level AgentSessionCapabilities shape was updated to attachments?: ..., but this later snippet still shows attachments as required. That leaves two conflicting contracts on the same page.

Suggested doc fix
 messaging: {
   receive: true;
   send?: boolean;
-  attachments: Array<'text' | 'image'>;
+  attachments?: Array<'text' | 'image'>;
 }
🤖 Prompt for 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.

In `@web/content/docs/session-capabilities.mdx` around lines 60 - 65, The
messaging snippet in the docs conflicts with the updated
AgentSessionCapabilities because it declares attachments as required; update the
messaging block (the messaging object example) so attachments is optional
(attachments?: Array<'text' | 'image'>) to match AgentSessionCapabilities and
keep the two contracts consistent.
🧹 Nitpick comments (1)
web/content/docs/migration.mdx (1)

69-75: ⚡ Quick win

Doc example is using the correct live-client token field; reconnect expects apiToken.

RelayAgentClient/RelayAgentHandle exposes the persisted credential as token?: string, while relay.workspace.reconnect is typed as reconnect(input: { apiToken: string }). So const persisted = alice.token; followed by reconnect({ apiToken: persisted }) matches the SDK.
Optional: rename persisted to persistedApiToken (or similar) to reduce token/apiToken terminology confusion in the snippet.

🤖 Prompt for 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.

In `@web/content/docs/migration.mdx` around lines 69 - 75, The snippet uses the
live-client field token but reconnect expects apiToken; update the example to
read the persisted credential from RelayAgentClient/RelayAgentHandle as const
persisted = alice.token and pass it to relay.workspace.reconnect as reconnect({
apiToken: persisted }); optionally rename persisted to persistedApiToken to
clarify token vs apiToken. Ensure you reference the
RelayAgentClient/RelayAgentHandle token property and the
relay.workspace.reconnect(input: { apiToken: string }) call when making the
change.
🤖 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 `@packages/cli/src/cli/agent-relay-mcp.ts`:
- Around line 502-523: Get the session first and preserve its selected identity
when building and using relay-backed clients: call getSession() before
getRelayAgentActions/getAgentClient, pass a wrapper to getRelayAgentActions that
always requests a client for the session's selected identity (e.g.,
getRelayAgentActions((id) => getAgentClient(session.selectedIdentity ?? id))),
and when falling back to the local actions.invoke set the caller name to
session.selectedIdentity ?? session.agentName; apply the same change pattern to
the other relay invocation sites referenced (the blocks around lines 545-564,
570-585, 592-603) so relay and fallback invocations run as the selected
participant.

In `@packages/harnesses/src/readme-quickstart.test.ts`:
- Around line 42-44: The test is still exercising the legacy listener API
(relay.on / relay.notify) instead of the documented v8 entry point; update the
harness in readme-quickstart.test.ts to use relay.addListener(...) everywhere it
currently calls relay.on(...) and replace relay.notify(...) call-sites with the
corresponding addListener-driven notification flow (register the listener via
relay.addListener with the same handler logic and use the relay workspace client
send/emit method appropriate for triggering that listener). Specifically, find
uses of relay.on, relay.notify, and any listener callback signatures, swap them
to relay.addListener (preserving the same event names/handler logic and return
values), and ensure the taskManagerClient.sendMessage invocation still targets
the new listener path so the test compiles against the documented API.

In `@packages/sdk/src/facade.ts`:
- Around line 149-164: RelayAgentClient currently omits the agent-scoped relay
surface built in assembleAgentClient/agentRelayAgent, so consumers can't call
methods like registerAction, addListener, or action; fix by extracting a shared
relay-surface interface (e.g., RelayAgentRelay or RelaySurface) that declares
registerAction, addListener, action, etc., and have RelayAgentClient extend that
interface (in addition to RelayAgentHandle and the messaging fields) so the
runtime methods exposed by assembleAgentClient are represented in the type
system.
- Around line 374-379: The returned unregister currently only calls
relayUnsubscribe() and localHandle.unregister(), leaving the published
relay/descriptor on the broker; update this unregister to also remove the
published descriptor created when the action was registered (the same
handle/descriptor returned by commands.register / the publish step). Locate the
descriptor/registration handle created during registration (the value
returned/stored when publishing the descriptor) and call its
unpublish/unregister/remove method from inside unregister (await it or handle
its promise and log errors), so the broker no longer advertises the action when
the local handler is removed.

In `@tests/integration/sdk/v8-api-smoke.mjs`:
- Around line 69-71: The DM and group-DM sends from alice.sendMessage are not
verified; after each send, await and assert that the recipient client(s) (e.g.,
Bob and Carol) actually received the DM and that a DM conversation/inbox entry
exists and contains the expected message/text and participants. Concretely, add
an assertion after await alice.sendMessage(...) that uses the recipient client
API (e.g., bob.waitForMessage or bob.on('message') / bob.getConversations or
bob.getInbox) to: 1) confirm a new DM conversation was created or updated, 2)
confirm the message text matches ('ping' or 'group ping'), and 3) for the group
DM assert both Bob and Carol have the conversation; also optionally verify the
seen/read flag is updated if applicable.
- Around line 83-85: Replace the fixed 1.5s sleep with a bounded polling loop
that waits until seen.length >= 2 or a real timeout elapses: record a deadline
(e.g., Date.now() + 5000), then loop sleeping short intervals (e.g., 50–200ms)
and break if seen.length >= 2 or the deadline passes; after the loop call
assert.ok(seen.length >= 2, `listener received channel messages (got
${seen.length})`) to fail with captured state. Update the code that currently
uses await new Promise((r) => setTimeout(r, 1500)) to this bounded-wait pattern
so the test is robust to latency variance while still timing out.

In `@web/content/docs/events.mdx`:
- Around line 78-83: The MessageEnvelope interface currently types parent as a
bare string, which conflicts with other v8 docs that treat envelope fields as
rich objects; change parent?: string to parent?: MessageRef (or whatever the
repo's message reference type is named) so parent is a rich object consistent
with from/to/channel, and update any examples/comments that reference
envelope.parent to use the MessageRef shape; look for the MessageEnvelope
declaration and any listener examples to ensure they read parent metadata as the
object form.
- Around line 49-57: The canonical RelayEvent union's 'message.reacted' variant
currently lists { messageId, emoji, by } but the reaction docs reference
event.agentName and event.action; update the 'message.reacted' variant in the
RelayEvent union to include agentName: string and action: string (preserving
messageId and emoji as appropriate) so the schema matches the examples — i.e.,
replace or augment the existing by/AgentRef field with agentName and add action
to match the emoji-reactions.mdx usage and ensure consumers can access
event.agentName and event.action.

In `@web/content/docs/introduction.mdx`:
- Around line 18-30: The intro sentence that references "three core jobs" is now
incorrect because the CardGroup (CardGroup, containing Cards titled "Messaging",
"Delivery", "Actions", and "Webhooks") presents four top-level capabilities;
update the lead-in sentence immediately above this CardGroup to reference four
core capabilities (or rephrase to a non-numeric "core capabilities") so it
accurately matches the Card titles and tone of the section.

In `@web/content/docs/threads.mdx`:
- Around line 54-56: The example uses relay.addListener('thread.reply') which
won't fire because the public API emits message.created events; update the event
selector to 'message.created' in the relay.addListener call (keep the existing
envelope.parent check that compares to messageId and the same handler signature
using { message, envelope } so the example still narrows replies correctly),
referencing the relay.addListener call, the handler parameters message and
envelope, and the envelope.parent check.

In `@web/content/docs/webhooks.mdx`:
- Around line 17-19: Update the docs to show the full return shape from
relay.webhooks.createInbound — change the text that currently says it returns `{
url, token }` to list the complete object `{ url, token, webhookId, channel }`
(and update any adjacent examples or copy that assume only url/token so they
reflect webhookId and channel fields); reference the createInbound call in the
sentence and ensure the documentation examples demonstrate that the returned
object includes webhookId and channel.
- Around line 47-49: The docs refer to event names inconsistently: replace the
non-conforming example event names (e.g., "agent.idle") with the status-oriented
vocabulary used elsewhere (e.g., "status.agent.idle" or the appropriate
"status.*" event name) and update the explanatory text that claims outbound
subscriptions share the `relay.addListener` vocabulary to explicitly state they
use the `status.*` namespace; update the examples and any other occurrences
(including the related block around the later 65-74 range) so
`relay.webhooks.subscribe(...)` and `relay.addListener` examples consistently
use `status.*` event names.

---

Outside diff comments:
In `@packages/openclaw/src/gateway.ts`:
- Around line 1866-1879: handlePolledEvent currently only handles legacy
'reaction.added' / 'reaction.removed' names; add support for the unified
'message.reacted' envelope by adding a case for 'message.reacted' in the same
switch and call handleRealtimeReaction(event.payload as MessageReactedEvent,
action, { ...baseOptions, eventId: event.id }) returning .committed; determine
the action string ('added' or 'removed') from event.payload.action (or map
payload fields if the unified payload shape differs) so the logic in
handleRealtimeReaction continues to work for both legacy and v8-normalized
events.

In `@web/content/docs/reference-openclaw.mdx`:
- Around line 25-32: The AgentSession type currently requires release(),
conflicting with session-capabilities docs that document lifecycle.release:
false; make release optional on the documented contract by changing the
AgentSession definition so release?: Promise<void> or a method signature marked
optional (i.e., release?(reason?: string): Promise<void>), and update any text
around AgentSession to reflect that adapters may omit the release method when
lifecycle.release is false; ensure references to AgentSession and release() in
this file are consistent with the optional contract.

In `@web/content/docs/session-capabilities.mdx`:
- Around line 60-65: The messaging snippet in the docs conflicts with the
updated AgentSessionCapabilities because it declares attachments as required;
update the messaging block (the messaging object example) so attachments is
optional (attachments?: Array<'text' | 'image'>) to match
AgentSessionCapabilities and keep the two contracts consistent.

---

Nitpick comments:
In `@web/content/docs/migration.mdx`:
- Around line 69-75: The snippet uses the live-client field token but reconnect
expects apiToken; update the example to read the persisted credential from
RelayAgentClient/RelayAgentHandle as const persisted = alice.token and pass it
to relay.workspace.reconnect as reconnect({ apiToken: persisted }); optionally
rename persisted to persistedApiToken to clarify token vs apiToken. Ensure you
reference the RelayAgentClient/RelayAgentHandle token property and the
relay.workspace.reconnect(input: { apiToken: string }) call when making the
change.
🪄 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: 2c8dbbe8-9f25-4d9c-abf8-c9e14ea704e1

📥 Commits

Reviewing files that changed from the base of the PR and between dd0320e and 226250a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (63)
  • .agentworkforce/trajectories/completed/2026-06/traj_07tr0h0sx6q8/summary.md
  • .agentworkforce/trajectories/completed/2026-06/traj_07tr0h0sx6q8/trajectory.json
  • .agentworkforce/trajectories/completed/2026-06/traj_duzh6woahe0d.trace.json
  • .agentworkforce/trajectories/completed/2026-06/traj_duzh6woahe0d/summary.md
  • .agentworkforce/trajectories/completed/2026-06/traj_duzh6woahe0d/trajectory.json
  • .agentworkforce/trajectories/completed/2026-06/traj_h4b8y58nrqnf/summary.md
  • .agentworkforce/trajectories/completed/2026-06/traj_h4b8y58nrqnf/trajectory.json
  • .agentworkforce/trajectories/completed/2026-06/traj_r01417xy0z6w.trace.json
  • .agentworkforce/trajectories/completed/2026-06/traj_r01417xy0z6w/summary.md
  • .agentworkforce/trajectories/completed/2026-06/traj_r01417xy0z6w/trajectory.json
  • .claude/rules/sdk.md
  • CHANGELOG.md
  • README.md
  • packages/cli/package.json
  • packages/cli/src/cli/agent-relay-mcp.ts
  • packages/cli/src/cli/lib/sdk-client.ts
  • packages/harnesses/src/create-human.test.ts
  • packages/harnesses/src/create-human.ts
  • packages/harnesses/src/index.ts
  • packages/harnesses/src/readme-quickstart.test.ts
  • packages/openclaw/package.json
  • packages/openclaw/src/__tests__/gateway-control.test.ts
  • packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts
  • packages/openclaw/src/__tests__/gateway-threads.test.ts
  • packages/openclaw/src/__tests__/ws-client.test.ts
  • packages/openclaw/src/gateway.ts
  • packages/sdk/package.json
  • packages/sdk/src/__tests__/facade.test.ts
  • packages/sdk/src/__tests__/listeners.test.ts
  • packages/sdk/src/__tests__/messaging.test.ts
  • packages/sdk/src/__tests__/register-action-relay.test.ts
  • packages/sdk/src/__tests__/webhooks.test.ts
  • packages/sdk/src/actions/json-schema-lite.ts
  • packages/sdk/src/agent-relay.ts
  • packages/sdk/src/facade.ts
  • packages/sdk/src/listeners.ts
  • packages/sdk/src/messaging/normalize.ts
  • packages/sdk/src/messaging/relaycast.ts
  • packages/sdk/src/messaging/types.ts
  • packages/sdk/src/session/types.ts
  • tests/integration/sdk/v8-api-smoke.mjs
  • web/content/docs/actions.mdx
  • web/content/docs/agent-relay-mcp.mdx
  • web/content/docs/channels.mdx
  • web/content/docs/delivery.mdx
  • web/content/docs/dms.mdx
  • web/content/docs/emoji-reactions.mdx
  • web/content/docs/event-handlers.mdx
  • web/content/docs/events.mdx
  • web/content/docs/harness-driver.mdx
  • web/content/docs/harnesses.mdx
  • web/content/docs/introduction.mdx
  • web/content/docs/messaging.mdx
  • web/content/docs/migration.mdx
  • web/content/docs/quickstart.mdx
  • web/content/docs/reference-openclaw.mdx
  • web/content/docs/sending-messages.mdx
  • web/content/docs/session-capabilities.mdx
  • web/content/docs/threads.mdx
  • web/content/docs/typescript-sdk.mdx
  • web/content/docs/webhooks.mdx
  • web/content/docs/workspaces.mdx
  • web/lib/docs-nav.ts
💤 Files with no reviewable changes (1)
  • packages/sdk/src/tests/messaging.test.ts

Comment on lines +502 to +523
const invokeAction = async (name: string, input: unknown) => {
const relayActions = getRelayAgentActions(getAgentClient);
if (relayActions) {
try {
const ack = await relayActions.invoke(name, asInputRecord(input));
return jsonContent({ ok: true, status: 'invoked', invocation: ack });
} catch (error) {
return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
}
}

const session = getSession();
const result = await actions.invoke({
name,
input,
context: {
caller: { name: session.agentName ?? 'mcp', type: 'agent' },
emit: onAuditEvent,
},
});
return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
};
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the selected identity when invoking relay-backed actions.

Every other mutating MCP tool accepts as, but this path now always calls getAgentClient() with the default identity and the local fallback stamps session.agentName as the caller. In multi-identity sessions, action invocations can no longer run as the selected participant, so attribution/authorization will drift to the wrong agent.

Suggested fix
-  const invokeAction = async (name: string, input: unknown) => {
-    const relayActions = getRelayAgentActions(getAgentClient);
+  const invokeAction = async (name: string, input: unknown, asIdentity?: string) => {
+    const relayActions = getRelayAgentActions(getAgentClient, asIdentity);
     if (relayActions) {
       try {
         const ack = await relayActions.invoke(name, asInputRecord(input));
         return jsonContent({ ok: true, status: 'invoked', invocation: ack });
       } catch (error) {
         return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
       }
     }

     const session = getSession();
     const result = await actions.invoke({
       name,
       input,
       context: {
-        caller: { name: session.agentName ?? 'mcp', type: 'agent' },
+        caller: { name: asIdentity ?? session.agentName ?? 'mcp', type: 'agent' },
         emit: onAuditEvent,
       },
     });
     return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
   };
       inputSchema: {
         name: z.string().describe('Registered action name'),
         input: z.unknown().describe('Action input payload'),
+        ...identityOverrideInputShape,
       },
...
-    async ({ name, input }: { name: string; input: unknown }) => invokeAction(name, input)
+    async ({ name, input, as }: { name: string; input: unknown; as?: string }) =>
+      invokeAction(name, input, as)
           {
             title: descriptor.name,
             description: descriptor.description,
-            inputSchema: actionToolInputSchema(descriptor.inputSchema),
+            inputSchema: {
+              ...actionToolInputSchema(descriptor.inputSchema),
+              ...identityOverrideInputShape,
+            },
             outputSchema: jsonResult,
...
-          async (args: unknown) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args))
+          async (args: unknown) =>
+            invokeAction(descriptor.name, actionInvocationInput(descriptor, args), readAsIdentity([args]))
 function getRelayAgentActions(
-  getAgentClient?: (asIdentity?: string) => AgentClientLike
+  getAgentClient?: (asIdentity?: string) => AgentClientLike,
+  asIdentity?: string
 ): AgentClientLike['actions'] | undefined {
...
-    return getAgentClient().actions;
+    return getAgentClient(asIdentity).actions;

Also applies to: 545-564, 570-585, 592-603

🤖 Prompt for 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.

In `@packages/cli/src/cli/agent-relay-mcp.ts` around lines 502 - 523, Get the
session first and preserve its selected identity when building and using
relay-backed clients: call getSession() before
getRelayAgentActions/getAgentClient, pass a wrapper to getRelayAgentActions that
always requests a client for the session's selected identity (e.g.,
getRelayAgentActions((id) => getAgentClient(session.selectedIdentity ?? id))),
and when falling back to the local actions.invoke set the caller name to
session.selectedIdentity ?? session.agentName; apply the same change pattern to
the other relay invocation sites referenced (the blocks around lines 545-564,
570-585, 592-603) so relay and fallback invocations run as the selected
participant.

Comment on lines +42 to +44
const taskManagerClient = await relay.workspace.register(taskManager);

await relay.sendMessage({
await taskManagerClient.sendMessage({
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Remaining old listener helpers in docs-facing examples =="
rg -n -C2 'relay\.(on|notify)\s*\(' packages/harnesses/src/readme-quickstart.test.ts README.md web/content/docs

echo
echo "== New listener API examples =="
rg -n -C2 'addListener\s*\(' packages/harnesses/src/readme-quickstart.test.ts README.md web/content/docs

Repository: AgentWorkforce/relay

Length of output: 34503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/harnesses/src/readme-quickstart.test.ts"
echo "== relay.on / relay.notify contexts in $FILE =="
rg -n 'relay\.(on|notify)\s*\(' "$FILE" -n
echo
echo "== Snippets around first few matches =="
python3 - <<'PY'
import re, itertools, pathlib
path = pathlib.Path("packages/harnesses/src/readme-quickstart.test.ts")
lines = path.read_text().splitlines()
pat = re.compile(r"relay\.(on|notify)\s*\(")
# collect line numbers
hits=[]
for i,l in enumerate(lines, start=1):
    if pat.search(l):
        hits.append(i)
# print around first 8 hits
for ln in hits[:8]:
    start=max(1, ln-6)
    end=min(len(lines), ln+6)
    print(f"\n--- {path}:{start}-{end} (hit at {ln}) ---")
    for j in range(start, end+1):
        print(f"{j:4d}: {lines[j-1]}")
PY

Repository: AgentWorkforce/relay

Length of output: 3641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/harnesses/src/readme-quickstart.test.ts"

echo "== File header / intent =="
head -n 80 "$FILE"

echo
echo "== Occurrences of relay.addListener in $FILE =="
rg -n 'relay\.addListener\s*\(' "$FILE" || true

echo
echo "== Occurrences of relay.on / relay.notify in $FILE =="
rg -n 'relay\.(on|notify)\s*\(' "$FILE"

echo
echo "== Snippet around the first relay.on (lines 40-90) =="
sed -n '35,90p' "$FILE"

echo
echo "== Snippet around listenerExample (lines 95-155) =="
sed -n '95,155p' "$FILE"

Repository: AgentWorkforce/relay

Length of output: 7018


Fix README quickstart listener contract to use relay.addListener(...)

packages/harnesses/src/readme-quickstart.test.ts is a compile-time proof for README API shapes, but it still uses relay.on(...) / relay.notify(...) (lines ~49-56, ~69-72, ~117-122, ~124-130) and contains no relay.addListener(...) usages. Since v8 docs define relay.addListener(...) as the single listener entry point, this test will validate the legacy listener surface instead of the documented one.

🤖 Prompt for 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.

In `@packages/harnesses/src/readme-quickstart.test.ts` around lines 42 - 44, The
test is still exercising the legacy listener API (relay.on / relay.notify)
instead of the documented v8 entry point; update the harness in
readme-quickstart.test.ts to use relay.addListener(...) everywhere it currently
calls relay.on(...) and replace relay.notify(...) call-sites with the
corresponding addListener-driven notification flow (register the listener via
relay.addListener with the same handler logic and use the relay workspace client
send/emit method appropriate for triggering that listener). Specifically, find
uses of relay.on, relay.notify, and any listener callback signatures, swap them
to relay.addListener (preserving the same event names/handler logic and return
values), and ensure the taskManagerClient.sendMessage invocation still targets
the new listener path so the test compiles against the documented API.

Comment on lines +149 to +164
/**
* A live, registered agent. Returned by `relay.workspace.register(...)` /
* `reconnect(...)` and by harness `create(...)`. Carries the agent's identity
* and status/tool predicate builders alongside a messaging surface scoped to
* that agent.
*/
export interface RelayAgentClient extends RelayAgentHandle {
readonly agents: RelayMessaging['agents'];
readonly channels: RelayMessaging['channels'];
readonly messages: EnrichedMessages;
readonly threads: RelayMessaging['threads'];
readonly inbox: RelayMessaging['inbox'];
sendMessage(input: RelaySendMessageInput): Promise<RelayMessage>;
reply(input: RelayReplyInput): Promise<RelayMessage>;
react(input: RelayClientReactInput): Promise<RelayMessageReaction>;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expose the live-client relay surface in RelayAgentClient.

workspace.register() / reconnect() are typed to return RelayAgentClient, but this interface only declares messaging helpers plus the handle fields. The actual object built in assembleAgentClient(...) also carries the relay surface from agentRelayAgent(...) (registerAction, addListener, action, etc.). As written, TypeScript consumers of the new v8 live-client API cannot call the agent-scoped action/listener methods that this PR is wiring up at runtime.

Consider extracting a shared base interface for the relay surface and having RelayAgentClient extend it rather than re-declaring only the messaging subset.

🤖 Prompt for 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.

In `@packages/sdk/src/facade.ts` around lines 149 - 164, RelayAgentClient
currently omits the agent-scoped relay surface built in
assembleAgentClient/agentRelayAgent, so consumers can't call methods like
registerAction, addListener, or action; fix by extracting a shared relay-surface
interface (e.g., RelayAgentRelay or RelaySurface) that declares registerAction,
addListener, action, etc., and have RelayAgentClient extend that interface (in
addition to RelayAgentHandle and the messaging fields) so the runtime methods
exposed by assembleAgentClient are represented in the type system.

Comment on lines +374 to +379
return {
unregister: () => {
relayUnsubscribe();
localHandle.unregister();
},
};
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the relay descriptor when an action is unregistered.

This unregister() only tears down the event subscription and local registry entry. The descriptor published earlier via commands.register(...) is left behind on the broker, so other agents can still discover/invoke an action that no longer has a local handler and its invocation will never complete.

Suggested fix
   return {
     unregister: () => {
+      void wiring?.messaging.commands.delete?.(def.name).catch((error) => {
+        console.error(`[agent-relay] failed to delete action descriptor "${def.name}":`, error);
+      });
       relayUnsubscribe();
       localHandle.unregister();
     },
   };
🤖 Prompt for 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.

In `@packages/sdk/src/facade.ts` around lines 374 - 379, The returned unregister
currently only calls relayUnsubscribe() and localHandle.unregister(), leaving
the published relay/descriptor on the broker; update this unregister to also
remove the published descriptor created when the action was registered (the same
handle/descriptor returned by commands.register / the publish step). Locate the
descriptor/registration handle created during registration (the value
returned/stored when publishing the descriptor) and call its
unpublish/unregister/remove method from inside unregister (await it or handle
its promise and log errors), so the broker no longer advertises the action when
the local handler is removed.

Comment on lines +69 to +71
// 9) DM and group DM routing
await alice.sendMessage({ to: '@Bob', text: 'ping' });
await alice.sendMessage({ to: ['@Bob', '@Carol'], text: 'group ping' });
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The DM and group-DM flow is exercised but never verified.

These sends do not feed seen and there is no inbox / conversation assertion afterward, so a routing regression here would still end with v8 smoke OK. Add an explicit check against the recipient clients’ DM state or a DM-specific listener before claiming coverage.

🤖 Prompt for 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.

In `@tests/integration/sdk/v8-api-smoke.mjs` around lines 69 - 71, The DM and
group-DM sends from alice.sendMessage are not verified; after each send, await
and assert that the recipient client(s) (e.g., Bob and Carol) actually received
the DM and that a DM conversation/inbox entry exists and contains the expected
message/text and participants. Concretely, add an assertion after await
alice.sendMessage(...) that uses the recipient client API (e.g.,
bob.waitForMessage or bob.on('message') / bob.getConversations or bob.getInbox)
to: 1) confirm a new DM conversation was created or updated, 2) confirm the
message text matches ('ping' or 'group ping'), and 3) for the group DM assert
both Bob and Carol have the conversation; also optionally verify the seen/read
flag is updated if applicable.

Comment on lines +78 to +83
interface MessageEnvelope {
from: AgentRef; // the sender
to?: AgentRef | AgentRef[]; // DM recipient, or group-DM recipients
channel?: ChannelRef; // present for channel posts and threads in a channel
parent?: string; // messageId this is a reply to, for thread replies
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep envelope.parent consistent with the rest of the v8 docs.

Here parent is documented as a bare string, but the listener docs describe the envelope as rich objects for from, to, channel, and parent. Leaving the canonical schema as string will make thread-handling examples disagree on how to read parent metadata.

🤖 Prompt for 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.

In `@web/content/docs/events.mdx` around lines 78 - 83, The MessageEnvelope
interface currently types parent as a bare string, which conflicts with other v8
docs that treat envelope fields as rich objects; change parent?: string to
parent?: MessageRef (or whatever the repo's message reference type is named) so
parent is a rich object consistent with from/to/channel, and update any
examples/comments that reference envelope.parent to use the MessageRef shape;
look for the MessageEnvelope declaration and any listener examples to ensure
they read parent metadata as the object form.

Comment on lines 18 to 30
<CardGroup cols={3}>
<Card title="Messaging" href="/docs/8.0.0/messaging">
<Card title="Messaging" href="/docs/messaging">
Channels, DMs, group DMs, threads, reactions, mentions, attachments, inbox state, and read state for agent coordination.
</Card>
<Card title="Delivery" href="/docs/8.0.0/delivery">
<Card title="Delivery" href="/docs/delivery">
A harness contract for getting messages into sessions at the right boundary, then reporting accepted, delivered, deferred, or failed receipts.
</Card>
<Card title="Actions" href="/docs/8.0.0/actions">
Typed capabilities with Zod schemas, policy checks, audit events, MCP tool generation, and structured result envelopes.
<Card title="Actions" href="/docs/actions">
Fire-and-forget typed capabilities with Zod schemas, MCP tool generation, and `action.completed` events.
</Card>
<Card title="Webhooks" href="/docs/8.0.0/webhooks">
Outgoing workspace event webhooks and incoming HTTP handlers that turn external events into Relay messages or actions.
<Card title="Webhooks" href="/docs/webhooks">
Inbound webhooks that turn external events into channel messages, and outbound HMAC-signed event delivery.
</Card>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the “three core jobs” framing to match this card set.

This block now presents four top-level capabilities (Messaging, Delivery, Actions, Webhooks), so the lead-in sentence above reads like something is missing.

🤖 Prompt for 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.

In `@web/content/docs/introduction.mdx` around lines 18 - 30, The intro sentence
that references "three core jobs" is now incorrect because the CardGroup
(CardGroup, containing Cards titled "Messaging", "Delivery", "Actions", and
"Webhooks") presents four top-level capabilities; update the lead-in sentence
immediately above this CardGroup to reference four core capabilities (or
rephrase to a non-numeric "core capabilities") so it accurately matches the Card
titles and tone of the section.

Comment on lines +54 to +56
const stop = relay.addListener('thread.reply', ({ message, envelope }) => {
if (envelope.parent !== messageId) return;
console.log(`Thread reply from ${envelope.from.handle}: ${message.text}`);
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the message event selector here, not thread.reply.

The rest of this PR documents message listeners with message.created, and this example already uses envelope.parent to narrow to replies. If the public event surface only emits message-created events for replies, this snippet never fires.

Suggested doc fix
-const stop = relay.addListener('thread.reply', ({ message, envelope }) => {
+const stop = relay.addListener('message.created', ({ message, envelope }) => {
   if (envelope.parent !== messageId) return;
   console.log(`Thread reply from ${envelope.from.handle}: ${message.text}`);
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stop = relay.addListener('thread.reply', ({ message, envelope }) => {
if (envelope.parent !== messageId) return;
console.log(`Thread reply from ${envelope.from.handle}: ${message.text}`);
const stop = relay.addListener('message.created', ({ message, envelope }) => {
if (envelope.parent !== messageId) return;
console.log(`Thread reply from ${envelope.from.handle}: ${message.text}`);
});
🤖 Prompt for 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.

In `@web/content/docs/threads.mdx` around lines 54 - 56, The example uses
relay.addListener('thread.reply') which won't fire because the public API emits
message.created events; update the event selector to 'message.created' in the
relay.addListener call (keep the existing envelope.parent check that compares to
messageId and the same handler signature using { message, envelope } so the
example still narrows replies correctly), referencing the relay.addListener
call, the handler parameters message and envelope, and the envelope.parent
check.

Comment on lines +17 to +19
`relay.webhooks.createInbound({ channel })` returns a `{ url, token }`. External services POST
`{ message, author }` to the URL with `Authorization: Bearer <token>`, and the message appears in the
channel instantly.
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the full createInbound() return shape.

The PR docs/objectives for this redesign describe createInbound({ channel }) returning { url, token, webhookId, channel }, but this sentence narrows it to { url, token }. That understates the public API right where readers are likely to copy it.

🤖 Prompt for 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.

In `@web/content/docs/webhooks.mdx` around lines 17 - 19, Update the docs to show
the full return shape from relay.webhooks.createInbound — change the text that
currently says it returns `{ url, token }` to list the complete object `{ url,
token, webhookId, channel }` (and update any adjacent examples or copy that
assume only url/token so they reflect webhookId and channel fields); reference
the createInbound call in the sentence and ensure the documentation examples
demonstrate that the returned object includes webhookId and channel.

Comment on lines +47 to +49
Subscribe your service to events such as `message.created`, `action.completed`, or `agent.idle`.
`relay.webhooks.subscribe(...)` registers the subscription; Relay POSTs each matching event to your URL with
an HMAC signature you verify using the shared secret.
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same status event vocabulary here as the rest of the docs.

This section says outbound subscriptions share the relay.addListener event vocabulary, but agent.idle does not match the status naming used elsewhere in the supplied docs (status.* events and status predicate builders). If users subscribe to agent.idle, they may never receive anything.

Suggested doc fix
-Subscribe your service to events such as `message.created`, `action.completed`, or `agent.idle`.
+Subscribe your service to events such as `message.created`, `action.completed`, or `status.idle`.
@@
-- `agent.idle`
+- `status.idle`

Also applies to: 65-74

🤖 Prompt for 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.

In `@web/content/docs/webhooks.mdx` around lines 47 - 49, The docs refer to event
names inconsistently: replace the non-conforming example event names (e.g.,
"agent.idle") with the status-oriented vocabulary used elsewhere (e.g.,
"status.agent.idle" or the appropriate "status.*" event name) and update the
explanatory text that claims outbound subscriptions share the
`relay.addListener` vocabulary to explicitly state they use the `status.*`
namespace; update the examples and any other occurrences (including the related
block around the later 65-74 range) so `relay.webhooks.subscribe(...)` and
`relay.addListener` examples consistently use `status.*` event names.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sdk/src/actions/json-schema-lite.ts">

<violation number="1" location="packages/sdk/src/actions/json-schema-lite.ts:437">
P2: Node version gap breaks zod schema conversion. `process.getBuiltinModule` is missing on Node 20.9–20.15, so this path returns undefined and action descriptors advertise permissive input schemas. Align runtime requirement or add a fallback loader.</violation>
</file>

<file name="packages/sdk/src/listeners.ts">

<violation number="1" location="packages/sdk/src/listeners.ts:422">
P1: Broad catch hides real connect failures. Only ignore workspace-only case; rethrow or log other errors.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// clients; a no-op (no stream) on workspace-only clients.
try {
context.events.connect();
} catch {
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: Broad catch hides real connect failures. Only ignore workspace-only case; rethrow or log other errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/src/listeners.ts, line 422:

<comment>Broad catch hides real connect failures. Only ignore workspace-only case; rethrow or log other errors.</comment>

<file context>
@@ -414,6 +414,14 @@ export function createListenerHub(
+    // clients; a no-op (no stream) on workspace-only clients.
+    try {
+      context.events.connect();
+    } catch {
+      // Workspace-only clients have no realtime stream; listen on an agent client.
+    }
</file context>

try {
// Resolved lazily to avoid a hard zod dependency (and a static node:module
// import) in the SDK.
const createRequire = nodeCreateRequire();
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: Node version gap breaks zod schema conversion. process.getBuiltinModule is missing on Node 20.9–20.15, so this path returns undefined and action descriptors advertise permissive input schemas. Align runtime requirement or add a fallback loader.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/src/actions/json-schema-lite.ts, line 437:

<comment>Node version gap breaks zod schema conversion. `process.getBuiltinModule` is missing on Node 20.9–20.15, so this path returns undefined and action descriptors advertise permissive input schemas. Align runtime requirement or add a fallback loader.</comment>

<file context>
@@ -415,7 +432,12 @@ function tryZodToJsonSchema(schema: object): Record<string, unknown> | undefined
-    // Resolved lazily to avoid a hard zod dependency in the SDK.
+    // Resolved lazily to avoid a hard zod dependency (and a static node:module
+    // import) in the SDK.
+    const createRequire = nodeCreateRequire();
+    if (!createRequire) {
+      return undefined;
</file context>

@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Reviewed the PR diff and traced the SDK/webhook/action paths.

Fixed:

  • registerAction relay wiring now starts the agent event stream so action.invoked handlers actually receive relay-routed invocations.
  • Webhook subscription normalization now accepts subscriptionId / subscription_id, not only id.
  • Added focused test coverage for both fixes.

Verified locally:

  • SDK source/build typecheck passed.
  • SDK focused suite passed: 69 tests.
  • OpenClaw focused suite passed: 82 tests.
  • Harness focused suite passed: 12 tests.

Some direct package-script runs were blocked by the interrupted local dependency install, so I used a temporary Vitest config and npx runner for focused verification.

@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Reviewed and fixed PR #1029 locally.

I changed the SDK relaycast adapter so missing webhook/subscription API errors name the public surface used: relay.webhooks now reports RelaycastMessagingClient.webhooks..., while existing integrations.webhooks / integrations.subscriptions diagnostics stay contextual. Added tests covering both surfaces.

Verification run:

  • npm --prefix packages/sdk test
  • npm --prefix packages/sdk run check
  • npm run typecheck
  • focused Vitest slice for SDK, CLI, config, and OpenClaw: 12 files / 260 tests passed
  • Prettier check on touched SDK files passed

Could not run Rust broker tests because cargo is not installed in this environment.

@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Reviewed and fixed PR #1029 locally.

Fixed:

  • OpenClaw gateway now uses relaycast 2.3 commandInvoked, reactionAdded, and reactionRemoved APIs.
  • OpenClaw tests now mock/fire the new event names and payload shapes.
  • Harness README compile-contract examples now use addListener and live registered clients.
  • packages/sdk/README.md no longer documents removed SDK surfaces.

Verified:

  • npm run typecheck
  • npm --prefix packages/sdk test ...
  • npx vitest run packages/harnesses/src/create-human.test.ts packages/harnesses/src/readme-quickstart.test.ts
  • npm --prefix packages/openclaw test ...

@willwashburn willwashburn merged commit 81bf2fc into main Jun 3, 2026
48 checks passed
@willwashburn willwashburn deleted the claude/upbeat-tesla-zqeKw branch June 3, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants