v8 SDK redesign: live agent clients, addListener, fire-and-forget actions, webhooks#1029
Conversation
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.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
CodeAnt AI is reviewing your PR. |
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughRefactors 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. ChangesSDK v8 API and webhooks end-to-end
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 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".
| }); | ||
|
|
||
| // Subscribe to invocations routed to this handler agent. | ||
| return wiring.messaging.events.on('actionInvoked', async (event) => { |
There was a problem hiding this comment.
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 👍 / 👎.
|
Preview deployed!
This preview will be cleaned up when the PR is merged or closed. |
| 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()); |
There was a problem hiding this comment.
🟠 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| 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 }; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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| 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))); |
There was a problem hiding this comment.
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| return { | ||
| unregister: () => { | ||
| relayUnsubscribe(); | ||
| localHandle.unregister(); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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| 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()); |
There was a problem hiding this comment.
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'; | ||
|
|
There was a problem hiding this comment.
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 finished reviewing your PR. |
|
Fixed one PR issue found during review:
Verified locally:
|
|
Reviewed and fixed PR #1029 fallout. Changed:
Verified:
Could not run full workspace CI locally because |
There was a problem hiding this comment.
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
| } catch { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
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>
| } 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}.`, |
There was a problem hiding this comment.
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>
| 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) { |
There was a problem hiding this comment.
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'>; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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>
| type: string; | |
| type: AgentSessionEvent['type']; |
| handler: ListenerHandler<RelayEvent> | ||
| ): (() => void) => { | ||
| if (typeof selector !== 'string') { | ||
| return selector.subscribe(context, handler as ListenerHandler); |
There was a problem hiding this comment.
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>
| ```ts | ||
| relay.registerAction({ | ||
| name: 'classify', | ||
| input: z.object({ foo: z.enum(['bar', 'bang']) }), |
There was a problem hiding this comment.
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.
|
Thanks for the thorough automated reviews — went through each. Pushed fixes in Fixed
Acknowledged — by design or tracked follow-up
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. |
There was a problem hiding this comment.
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 winHandle unified
message.reactedevents in poll fallback too.The realtime path now consumes
messageReacted, buthandlePolledEvent()still only recognizesreaction.added/reaction.removed. If the v8 poll envelope is already normalized tomessage.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 winMake
release()optional in the documented session contract.This block still makes
release()mandatory, butweb/content/docs/session-capabilities.mdxnow documentslifecycle.release: falsewith 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 winKeep the
attachmentsfield optional in the messaging snippet.The top-level
AgentSessionCapabilitiesshape was updated toattachments?: ..., but this later snippet still showsattachmentsas 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 winDoc example is using the correct live-client token field; reconnect expects
apiToken.
RelayAgentClient/RelayAgentHandleexposes the persisted credential astoken?: string, whilerelay.workspace.reconnectis typed asreconnect(input: { apiToken: string }). Soconst persisted = alice.token;followed byreconnect({ apiToken: persisted })matches the SDK.
Optional: renamepersistedtopersistedApiToken(or similar) to reducetoken/apiTokenterminology 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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.mdCHANGELOG.mdREADME.mdpackages/cli/package.jsonpackages/cli/src/cli/agent-relay-mcp.tspackages/cli/src/cli/lib/sdk-client.tspackages/harnesses/src/create-human.test.tspackages/harnesses/src/create-human.tspackages/harnesses/src/index.tspackages/harnesses/src/readme-quickstart.test.tspackages/openclaw/package.jsonpackages/openclaw/src/__tests__/gateway-control.test.tspackages/openclaw/src/__tests__/gateway-poll-fallback.test.tspackages/openclaw/src/__tests__/gateway-threads.test.tspackages/openclaw/src/__tests__/ws-client.test.tspackages/openclaw/src/gateway.tspackages/sdk/package.jsonpackages/sdk/src/__tests__/facade.test.tspackages/sdk/src/__tests__/listeners.test.tspackages/sdk/src/__tests__/messaging.test.tspackages/sdk/src/__tests__/register-action-relay.test.tspackages/sdk/src/__tests__/webhooks.test.tspackages/sdk/src/actions/json-schema-lite.tspackages/sdk/src/agent-relay.tspackages/sdk/src/facade.tspackages/sdk/src/listeners.tspackages/sdk/src/messaging/normalize.tspackages/sdk/src/messaging/relaycast.tspackages/sdk/src/messaging/types.tspackages/sdk/src/session/types.tstests/integration/sdk/v8-api-smoke.mjsweb/content/docs/actions.mdxweb/content/docs/agent-relay-mcp.mdxweb/content/docs/channels.mdxweb/content/docs/delivery.mdxweb/content/docs/dms.mdxweb/content/docs/emoji-reactions.mdxweb/content/docs/event-handlers.mdxweb/content/docs/events.mdxweb/content/docs/harness-driver.mdxweb/content/docs/harnesses.mdxweb/content/docs/introduction.mdxweb/content/docs/messaging.mdxweb/content/docs/migration.mdxweb/content/docs/quickstart.mdxweb/content/docs/reference-openclaw.mdxweb/content/docs/sending-messages.mdxweb/content/docs/session-capabilities.mdxweb/content/docs/threads.mdxweb/content/docs/typescript-sdk.mdxweb/content/docs/webhooks.mdxweb/content/docs/workspaces.mdxweb/lib/docs-nav.ts
💤 Files with no reviewable changes (1)
- packages/sdk/src/tests/messaging.test.ts
| 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 }; | ||
| }; |
There was a problem hiding this comment.
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.
| const taskManagerClient = await relay.workspace.register(taskManager); | ||
|
|
||
| await relay.sendMessage({ | ||
| await taskManagerClient.sendMessage({ |
There was a problem hiding this comment.
🧩 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/docsRepository: 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]}")
PYRepository: 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.
| /** | ||
| * 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>; | ||
| } |
There was a problem hiding this comment.
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.
| return { | ||
| unregister: () => { | ||
| relayUnsubscribe(); | ||
| localHandle.unregister(); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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.
| // 9) DM and group DM routing | ||
| await alice.sendMessage({ to: '@Bob', text: 'ping' }); | ||
| await alice.sendMessage({ to: ['@Bob', '@Carol'], text: 'group ping' }); |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| const stop = relay.addListener('thread.reply', ({ message, envelope }) => { | ||
| if (envelope.parent !== messageId) return; | ||
| console.log(`Thread reply from ${envelope.from.handle}: ${message.text}`); |
There was a problem hiding this comment.
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.
| 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.
| `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. |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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>
|
Reviewed the PR diff and traced the SDK/webhook/action paths. Fixed:
Verified locally:
Some direct package-script runs were blocked by the interrupted local dependency install, so I used a temporary Vitest config and |
|
Reviewed and fixed PR #1029 locally. I changed the SDK relaycast adapter so missing webhook/subscription API errors name the public surface used: Verification run:
Could not run Rust broker tests because |
|
Reviewed and fixed PR #1029 locally. Fixed:
Verified:
|
User description
Summary
Implements the v8
@agent-relay/sdkAPI 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 acrosssdk,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/sdk2.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()→ relaycastGET /v1/agent).AgentRelay.as()/asAgent().Messaging
agent.sendMessage({ to })(#channel/@handle/['@a','@b']group DM),agent.reply({ messageId }),agent.react({ messageId, emoji }).messageId. Removed the top-levelrelay.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 richenvelope(from/to/channel/parent).relay.on/relay.notify.Actions (fire-and-forget)
relay.registerAction(...)registers a descriptor on the relay and runs the handler onaction.invoked(loads input viagetInvocation, runs the localActionRegistryas validator/executor, postscompleteInvocation). Results surface asaction.completedevents; the invoking agent gets an ack, not the return value. The MCP invokes through the relay. Removed the publicrelay.actionsnamespace.Harnesses & humans
createHuman({ relay, name })self-registers and returns the live client;defineHarnessre-exported from@agent-relay/harnesses.AgentSession.releaseis optional, paired withcapabilities.lifecycle.release.Webhooks
relay.webhooksnamespace:createInbound({ channel })→{ url, token };subscribe({ url, events, secret, headers })(HMAC).relay.integrationsleft intact for provider connections.Supporting
@relaycast/sdkto 2.3.x; migratedopenclawto the unifiedmessage.reactedevent.events.mdxare the spec of record; 20web/content/docs/*.mdxpages rewritten (incl.migration.mdxas a from-old/to-v8 guide); CHANGELOG updated with Breaking Changes + Migration Guidance.Testing
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.mjsagainst a live workspace is the remaining validation step. The matching service contract was filed as AgentWorkforce/relaycast#158 and shipped in@relaycast/sdk2.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
messageIdfor replies and reactions, andtocan route to a channel, a direct message, or a group DMaddListenerentry point with dotted event names, wildcards, or predicates, and message events include a richer sender/channel envelopeImpact
✅ 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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
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:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
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.