Skip to content

fix(sdk-typescript): reuse the direct node token across reconnects - #441

Merged
AgentRelayBot merged 1 commit into
mainfrom
fix/reuse-direct-node-token
Sep 17, 2026
Merged

AgentRelayBot merged 1 commit into
mainfrom
fix/reuse-direct-node-token

Conversation

@AgentRelayBot

@AgentRelayBot AgentRelayBot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #440. That PR stopped a transient admission denial from permanently breaking an agent bind; this one addresses why the workspace was saturated in the first place.

Cause

AgentClient.connect() hands the WebSocket client a token factory:

token: () => this.fetchDirectNodeToken(),

So every connect attempt mints a fresh direct node token. That call is not free — it upserts the agent's direct node and rotates its token, which correctly places POST /v1/agent/node-token on the workspace's shared write-admission lane, alongside ordinary messaging and only two concurrent leases.

This makes reconnects self-amplifying. A denied mint rejects openSocket(), which schedules a reconnect, which mints again. Write backpressure generates more write load.

A production tail of relaycast-cloud-api caught it happening. In one 70-second window, a single workspace produced 47 node-token calls — 51/minute, median 329ms apart — every one of them from @relaycast/sdk@5.0.5 against an 8.11.0 engine. Twenty were denied. That load pushed the workspace's write lane to roughly 40% denial, which is what blocked an unrelated POST /v1/nodes/{name}/agents and left a freshly spawned agent unbound.

Backoff does not rescue this: handleOpen resets reconnectAttempt = 0 as soon as a socket opens, so a connection that opens and then drops restarts at the base delay every cycle.

Fix

A node token is a long-lived credential — the nodes table stores a token_hash with no expiry column — so it is meant to be held and reused, not re-minted per connect.

The token is now cached and reused across attempts. Reuse is bounded to three consecutive attempts that never reach a usable connection, so a token the server no longer accepts is still replaced rather than wedging the client. The counter resets once a connection actually opens and registers, which means a healthy client mints once per session no matter how often it reconnects.

The reset hangs off the open event, which the WS client emits only after node registration is sent. So the token is marked good when the connection is genuinely usable, not merely when the socket handshake completes.

Tests

Two new cases in agent-ws.test.ts: attempts that never open reuse the cached token and then re-mint once reuse is exhausted, and a connection that opens and registers repeatedly keeps a single minted token.

474 tests pass, tsc --noEmit is clean, and eslint reports no errors.

Operational note

The @relaycast/sdk@5.0.5 client driving this is pinned in cloud/package.json and cloud-canary/package.json, three majors behind the current 8.11.0. This fix reduces the blast radius of such a client, but upgrading it is the immediate relief.

Made with Cursor

Review in cubic

`AgentClient` passed the WebSocket client a token factory, so every connect
attempt minted a fresh direct node token. Minting is not free: it upserts
the agent's direct node and rotates its token, which places it on the
workspace's shared write-admission lane alongside ordinary messaging.

That made reconnects self-amplifying. A denied mint fails the connect,
which schedules a reconnect, which mints again — so write backpressure
generated more write load. Observed in production: an outdated client
looping at 51 mints/minute drove its workspace's write lane to ~40%
denial, which then blocked an unrelated agent/node bind.

A node token is a long-lived credential with no expiry, so cache and reuse
it. Reuse is bounded to three consecutive attempts that never reach a
usable connection, so a token the server no longer accepts is still
replaced; the counter resets once a connection opens and registers, which
means a healthy client mints once per session.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 87e07354-21ef-4642-bd88-284c2118c649


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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@AgentRelayBot
AgentRelayBot merged commit 683e4dc into main Sep 17, 2026
9 checks passed
@AgentRelayBot
AgentRelayBot deleted the fix/reuse-direct-node-token branch September 17, 2026 06:40
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