sdk: properly reject pending agent-ready promises and surface structured server errors#388
Conversation
…layServerError with structured context, add onError test, handle unhandled rejections Co-authored-by: willwashburn <957608+willwashburn@users.noreply.github.com>
Co-authored-by: willwashburn <957608+willwashburn@users.noreply.github.com>
0bc1375
into
fix/sdk-standalone-spawn-defaults
There was a problem hiding this comment.
Devin Review found 2 potential issues.
⚠️ 1 issue in files not directly in the diff
⚠️ RelayServerError not exported from SDK main entry point (packages/sdk/src/index.ts:176-184)
The new RelayServerError class is defined in packages/sdk/src/errors.ts and used internally by client.ts, but it is not exported from packages/sdk/src/index.ts. All other error classes (RelayError, DaemonNotRunningError, AgentNotFoundError, etc.) are exported from the main entry point at packages/sdk/src/index.ts:176-184, but RelayServerError is missing.
Impact on SDK consumers
The PR description shows the intended usage pattern:
client.onError = (error: Error) => {
if (error instanceof RelayServerError) {
console.log(`Server error [${error.code}]:`, error.message);
}
};Consumers importing from @agent-relay/sdk (the standard import path) will not have access to RelayServerError. They would need to know to use the deep import @agent-relay/sdk/errors instead, which is inconsistent with how all other error types are exported. This makes the new structured error feature effectively undiscoverable for most users.
View 5 additional findings in Devin Review.
| export class RelayServerError extends RelayError { | ||
| code: string; | ||
| fatal: boolean; | ||
| envelope?: any; | ||
|
|
||
| constructor(message: string, code: string, fatal: boolean, envelope?: any) { | ||
| super(message); | ||
| this.name = 'RelayServerError'; | ||
| this.code = code; | ||
| this.fatal = fatal; | ||
| this.envelope = envelope; | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Duplicate RelayServerError class definitions cause instanceof failures across packages
RelayServerError is defined as two separate classes: one in packages/utils/src/errors.ts:58-70 and another in packages/sdk/src/errors.ts:22-34. The SDK's client.ts imports from ./errors.js (the SDK-local copy), but @agent-relay/utils/errors also exports its own RelayServerError.
Root Cause and Impact
The established pattern in this codebase is that @agent-relay/utils/errors is the "single source of truth" for error classes (as stated in the file header comments at packages/sdk/src/errors.ts:4-6). All other error classes in the SDK's errors.ts are re-exported from @agent-relay/utils/errors. However, RelayServerError breaks this pattern by being defined locally in the SDK while also being added to @agent-relay/utils/errors.
Because these are two distinct JavaScript class constructors, instanceof checks will fail when comparing across packages. For example, if any code in the @agent-relay/utils or @agent-relay/daemon packages creates a RelayServerError using their import from @agent-relay/utils/errors, and SDK consumer code checks error instanceof RelayServerError using the SDK's version, the check will return false.
The SDK's errors.ts should re-export RelayServerError from @agent-relay/utils/errors instead of defining its own copy, consistent with how all other error classes are handled.
Prompt for agents
Remove the local RelayServerError class definition from packages/sdk/src/errors.ts (lines 19-34) and instead re-export it from @agent-relay/utils/errors, consistent with how all other error classes are handled. Change the export block at lines 9-17 to include RelayServerError:
export {
RelayError,
DaemonNotRunningError,
AgentNotFoundError,
TimeoutError,
ConnectionError,
ChannelNotFoundError,
SpawnError,
RelayServerError,
} from '@agent-relay/utils/errors';
Also remove the now-unnecessary local import of RelayError on line 20 and the entire local class definition on lines 22-34.
Was this helpful? React with 👍 or 👎 to provide feedback.
Addresses review feedback on PR #387:
clearPendingAgentReady()was leaving promises hanging forever instead of rejecting them, andonErrorcallback was passing generic errors without structured context from the daemon.Changes
Replace
clearPendingAgentReady()with proper rejection: Changed torejectPendingAgentReady(err)so callers receive errors instead of permanently pending promisesSurface structured server errors: Created
RelayServerErrorclass withcode,fatal, andenvelopefields. UpdatedonErrorcallback to pass this instead of genericError:Fix unhandled rejections in spawn: Added error handling when spawn fails with
waitForReady: trueto suppress unhandled rejection from the internalreadyPromiseAdd test coverage: New test validates
onErrorcallback receivesRelayServerErrorwith all structured fields💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.