Skip to content

feat(sdk): idempotent registration, typed errors, onError hook, listener narrowing#1074

Merged
willwashburn merged 3 commits into
mainfrom
feature/sdk-ergonomics
Jun 11, 2026
Merged

feat(sdk): idempotent registration, typed errors, onError hook, listener narrowing#1074
willwashburn merged 3 commits into
mainfrom
feature/sdk-ergonomics

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Four backward-compatible ergonomics fixes in packages/sdk.

1. Idempotent agent registration

relay.workspace.register(name) no longer throws name_conflict on every app restart. The facade now routes through the relaycast registerOrRotate API: when the name already exists, the existing identity is adopted and its token rotated.

  • Idempotent by default, per the restart-loop pain. Note the trade-off: rotation invalidates any previously-issued token for that agent (a second process registering the same name kicks the first one's token).
  • register(agents, { strict: true }) restores the old fail-on-conflict behavior.
  • RelayMessagingClient.agents.registerOrRotate is optional; backends without it (and injected mocks) fall back to plain register.

2. Typed errors

  • RelayError and RelayErrorCode (8 coded errors + retryable) are re-exported from the @agent-relay/sdk root.
  • Bare Error throws replaced where a sensible code exists: in-batch duplicate names in register()name_conflict; createWorkspace response missing a workspace key → transport_error (retryable: false).
  • Left as bare Error: pure usage errors with no matching code (register() is only available on the workspace client, unresolvable agent/message references).
  • relaycast-errors.ts: the typed code path (upstream PR fix(dashboard): enable mobile touch scrolling in log viewers #137) has shipped in @relaycast/sdk 2.5.x and is the primary signal; the structural fallback is kept (and documented as such) because it still catches errors that crossed HTTP/MCP serialization boundaries and lost their RelayError shape.

3. Error visibility for handlers

  • New onError hook: new AgentRelay({ onError }) or relay.onError(cb) (returns an unsubscribe). Invoked with (error, context) where context identifies the listener selector ({ source: 'listener', selector }) or action name ({ source: 'action', action, operation }).
  • Listener handler errors were previously swallowed with catch(() => {}); with no hook they now log a console.warn naming the listener.
  • Action wiring errors (register, connect, load_invocation, complete_invocation) route through the hook; default stays console.error.
  • Hooks are isolated: a throwing hook cannot break the event source.

4. Typed listener narrowing + once

  • New RelayEventMap: exact dotted selectors narrow the handler parameter (addListener('message.created', e => ...) gives RelayMessageEvent<'message.created'>); wildcards ('*', 'message.*') and unknown strings keep the full RelayEvent union.
  • relay.once(selector, handler) auto-unsubscribes after the first match (works with predicates too).
  • The predicate-builder API and broad (e: RelayEvent) => void handlers continue to compile unchanged.

Tests

  • npm test (packages/sdk): 9 files, 91 tests passed — includes new tests for register idempotency/strict/fallback/duplicate name_conflict, onError invocation (sync + async, constructor + method, hook isolation), default-warn behavior, once() semantics, action-wiring onError routing, registerOrRotate delegation in the relaycast client, and the typed createWorkspace error.
  • npm run test:types (new): vitest --typecheck over listeners.test-d.ts5 type tests, no errors; verified non-vacuous (a deliberately wrong assertion fails the run). Uses a dedicated tsconfig.typetest.json because the package tsconfig excludes __tests__.
  • npm run check and npm run build: clean.

🤖 Generated with Claude Code

…ner narrowing

Four backward-compatible ergonomics fixes in @agent-relay/sdk:

- relay.workspace.register() is idempotent by default via the relaycast
  registerOrRotate API: re-registering an existing name adopts the
  identity and rotates its token instead of throwing name_conflict.
  Pass { strict: true } to restore fail-on-conflict.
- Re-export RelayError and RelayErrorCode from the package root and use
  typed errors in the facade where a code applies (in-batch duplicate
  register -> name_conflict, missing workspace key -> transport_error).
- Add an onError hook (constructor option or relay.onError(cb)) that
  receives listener and action handler errors with a context naming the
  listener selector or action name. Without a hook, handler errors log
  a console warning instead of being silently swallowed.
- Add a RelayEventMap so exact dotted selectors narrow the handler event
  type in addListener, plus relay.once(selector, handler) which
  auto-unsubscribes after the first matching event. Wildcards and the
  predicate-builder API keep their existing types.

Adds unit tests for register idempotency, onError routing, and once()
semantics, plus a vitest --typecheck suite (npm run test:types) for the
selector narrowing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@willwashburn willwashburn requested a review from khaliqgant as a code owner June 9, 2026 20:06
@codeant-ai

codeant-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@gemini-code-assist

Copy link
Copy Markdown

Warning

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

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c140ba91-44ff-468a-9a9e-5b02da7bd776

📥 Commits

Reviewing files that changed from the base of the PR and between 05a57f5 and 4ed46c8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • CHANGELOG.md
  • packages/sdk/package.json
  • packages/sdk/src/__tests__/agent-relay.test.ts
  • packages/sdk/src/__tests__/facade.test.ts
  • packages/sdk/src/__tests__/listeners.test-d.ts
  • packages/sdk/src/__tests__/listeners.test.ts
  • packages/sdk/src/__tests__/messaging.test.ts
  • packages/sdk/src/__tests__/register-action-relay.test.ts
  • packages/sdk/src/agent-relay.ts
  • packages/sdk/src/facade.ts
  • packages/sdk/src/index.ts
  • packages/sdk/src/listeners.ts
  • packages/sdk/src/messaging/relaycast.ts
  • packages/sdk/src/messaging/types.ts
  • packages/sdk/src/relaycast-errors.ts
  • packages/sdk/tsconfig.typetest.json

📝 Walkthrough

Walkthrough

The SDK adds structured error handling for listener and action handlers, introduces typed selector-based listener registration with auto-unsubscribe, makes workspace agent registration idempotent with token rotation, and publishes RelayError as public API.

Changes

Relay SDK runtime and API behavior changes

Layer / File(s) Summary
Listener error context and typed selector contracts
packages/sdk/src/listeners.ts, packages/sdk/src/agent-relay.ts
New RelayErrorContext, RelayErrorHook, and logRelayHandlerError types centralize error reporting. RelayEventMap maps exact dotted selectors to event types, enabling type-narrowed overloads on ListenerHub.addListener/once and AgentRelay.addListener/once.
Listener runtime error reporting and once subscription
packages/sdk/src/listeners.ts, packages/sdk/src/__tests__/listeners.test.ts, packages/sdk/src/__tests__/listeners.test-d.ts
Predicates now route handler errors through selector-specific reporters instead of silently swallowing. ListenerHub.addListener and new once method both accept error hooks and construct reporters for messaging, action, and status events. Comprehensive runtime tests validate sync/async error routing, hook unsubscribe semantics, and default console warnings; type tests confirm selector narrowing for exact selectors and broad typing for wildcards.
AgentRelay onError hook wiring and agent client assembly
packages/sdk/src/agent-relay.ts
AgentRelay stores registered error hooks, forwards them into listener hub creation and agent client assembly via new AgentRelayAgentOptions.onError. New relay.onError(hook) method registers/unregisters hooks; reportError invokes all hooks with context or falls back to default logger when none are registered. Agent reconnection and action registration wire hooks consistently across client lifetimes.
Workspace registration idempotency and register-or-rotate support
packages/sdk/src/facade.ts, packages/sdk/src/messaging/types.ts, packages/sdk/src/messaging/relaycast.ts, packages/sdk/src/__tests__/agent-relay.test.ts, packages/sdk/src/__tests__/facade.test.ts, packages/sdk/src/__tests__/messaging.test.ts
RelayMessagingClient.agents gains optional registerOrRotate method; RelaycastMessagingClient implements it by delegating to Relaycast's API or falling back to register. New RelayRegisterOptions.strict flag controls strategy selection: default (idempotent) uses registerOrRotate to adopt existing identity and rotate token; strict: true uses plain register and rejects name conflicts with RelayError('name_conflict'). Workspace creation validates workspace key and throws RelayError('transport_error', { retryable: false }) on missing response data. Tests validate rotation behavior, fallback when API unavailable, batch duplicate detection, and transport error handling.
Action wiring error hooks and reporting integration
packages/sdk/src/facade.ts, packages/sdk/src/__tests__/register-action-relay.test.ts
ActionRelayWiring includes optional onError hook; new reportActionError helper routes failures through hook (with exception isolation) or console.error. All action wiring sites (descriptor registration, event stream connection, invocation loading/completion) now use reportActionError instead of direct logging. Test validates hook invocation with error context including action name and operation type.
Public API exports, type-test tooling, and release documentation
packages/sdk/src/index.ts, packages/sdk/package.json, packages/sdk/tsconfig.typetest.json, CHANGELOG.md, packages/sdk/src/relaycast-errors.ts
SDK root re-exports RelayError and RelayErrorCode from @relaycast/sdk. New test:types npm script runs Vitest type-check-only mode with tsconfig.typetest.json covering SDK sources and type tests. CHANGELOG documents new relay.once(...), relay.onError(...), registration idempotency, handler type narrowing, and listener error-reporting behavior. Error detection documentation emphasizes typed RelayError.code recognition with structural fallback across serialization boundaries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/relay#1082: Updates ListenerHub.addListener overloads and action event typing in packages/sdk/src/listeners.ts; the main PR builds on those typed selector and action predicate changes by adding error hooks and type-based handler narrowing.
  • AgentWorkforce/relay#1029: Implements v8 listener redesign affecting addListener/once and agent client behavior; the main PR's error handling and once-listener features extend that refactored listener and wiring surface.

Suggested labels

size:XXL

Suggested reviewers

  • khaliqgant

Poem

🐰 Hop through errors with style and grace,
One-time listeners find their place,
Typed selectors guide the way,
Register once, rotate today!
Errors caught, no more sneaking,
The SDK's improvements are peaking! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the four main changes: idempotent registration, typed errors, onError hook, and listener narrowing with exact focus on SDK improvements.
Description check ✅ Passed The description comprehensively covers all four features with implementation details, trade-offs, test results, and backward compatibility notes, though it deviates from the template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sdk-ergonomics

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a53872fcb2

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread packages/sdk/src/messaging/relaycast.ts Outdated
Comment on lines +449 to +451
this.relaycast.registerOrRotate
? await this.relaycast.registerOrRotate(input)
: await this.relaycast.agents.register(input)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Call registerOrRotate on the agents surface

For real RelayCast instances the rotate API is exposed as relay.agents.registerOrRotate (the existing CLI uses that shape in packages/cli/src/cli/agent-relay-mcp.ts:706), but this adapter checks a top-level this.relaycast.registerOrRotate. In production that check is false, so relay.workspace.register(...) falls back to plain agents.register and still throws name_conflict instead of performing the new idempotent rotate path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6ee24d5: RelaycastMessagingClient now checks relaycast.agents.registerOrRotate and falls back to agents.register only when that method is unavailable. I also updated the messaging test to model the real agents.registerOrRotate surface.

@willwashburn willwashburn merged commit ffabf6b into main Jun 11, 2026
2 checks passed
@willwashburn willwashburn deleted the feature/sdk-ergonomics branch June 11, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant