Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `agent-relay mcp` standalone binaries no longer dispatch each tool call twice.
- MCP `send_dm` now forwards idempotency keys so keyed retries do not store duplicate messages.
- `@agent-relay/connect join` returns as soon as the probe admits the participant.
- Timed-out or still-pending `@agent-relay/connect join` calls exit with status 8 and instruct the caller to retry the same request.

## [13.0.1] - 2026-10-02

Expand Down
23 changes: 5 additions & 18 deletions packages/connect/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ function errorDetails(error) {
connect_unavailable: ['Relay Connect cloud service is unavailable; retry once.', 8],
connect_not_joined: ['This agent session has not joined a Relay Connect.', 4],
connect_already_joined: ['This agent session is already in a different Relay Connect.', 5],
connect_join_pending: [
'A previous join has an unknown outcome; retry with the same link and options.',
8,
],
connect_join_timeout: ['Relay Connect join timed out; retry the same join safely.', 8],
connect_unreachable: ['Relay Connect cannot reach Cloud or Relaycast; retry once.', 8],
agent_token_invalid: ['Relay Connect is over; run leave once to clear the local registration.', 3],
not_a_relay_session: ['Run this command from a live Claude Code or Codex session.', 6],
Expand Down Expand Up @@ -205,24 +210,6 @@ async function run(options) {
);
delete body.host_claim;

if (response.data?.role !== 'host') {
const agentName = response.data?.agent_name;
const host = response.data?.host?.agent_name;
try {
if (!agentName || !host) throw new Error('Join response did not identify this agent and its host.');
requireOk(
await requestJson(probe.socketPath, {
method: 'POST',
path: `/connect/send?to=${encodeURIComponent(host)}`,
body: `${agentName} joined this Relay Connect and is ready to help.`,
})
);
} catch (error) {
const warning = errorDetails(error).message;
process.stderr.write(`Joined, but could not notify the host: ${warning}\n`);
}
}

if (options.json) printJson(response);
else process.stdout.write(`${joinSummary(response)}\n`);
return;
Expand Down
59 changes: 29 additions & 30 deletions packages/connect/tests/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function runCli(home: string, args: string[], input = '') {
}

describe('@agent-relay/connect CLI', () => {
it('joins through a fake socket, keeps the host claim private, sends one hello, and is repeatable', async () => {
it('joins through a fake socket, keeps the host claim private, and makes no blocking follow-up send', async () => {
const joins: Array<Record<string, string>> = [];
const hellos: string[] = [];
const { home } = await listen((request, response, body) => {
Expand All @@ -123,9 +123,9 @@ describe('@agent-relay/connect CLI', () => {
participants: [],
},
});
} else if (request.url === '/connect/send?to=host-agent') {
} else if (request.url?.startsWith('/connect/send')) {
hellos.push(body);
json(response, { ok: true, data: { sent: [{ to: 'host-agent', message_id: 'message-1' }] } });
json(response, { ok: false, error: { code: 'unexpected_send' } }, 500);
} else {
json(response, { ok: false, error: { code: 'unexpected', message: request.url } }, 404);
}
Expand All @@ -150,10 +150,7 @@ describe('@agent-relay/connect CLI', () => {
{ link: 'connect-1', name: 'guest-one', host_claim: claim },
{ link: 'connect-1' },
]);
expect(hellos).toEqual([
'guest-one joined this Relay Connect and is ready to help.',
'guest-one joined this Relay Connect and is ready to help.',
]);
expect(hellos).toEqual([]);
});

it('wraps send, status, and leave routes', async () => {
Expand Down Expand Up @@ -220,36 +217,38 @@ describe('@agent-relay/connect CLI', () => {
expect(sendRequests).toBe(0);
});

it('keeps a completed join successful when the host hello fails', async () => {
const { home } = await listen((request, response) => {
it('maps retry-safe join timeout and pending-operation errors', async () => {
const { home } = await listen((request, response, body) => {
if (request.url === '/setup/status') {
json(response, { ok: true, data: { version: '2026.10.4' } });
} else if (request.url === '/connect/join') {
json(response, {
ok: true,
data: {
connect_id: 'connect-joined',
agent_name: 'guest-joined',
role: 'guest',
task: 'Keep the join result',
expires_at: 'later',
host: { agent_name: 'host-left' },
const pending = JSON.parse(body).link === 'connect-pending';
json(
response,
{
ok: false,
error: {
code: pending ? 'connect_join_pending' : 'connect_join_timeout',
message: 'safe to retry',
},
},
});
} else if (request.url === '/connect/send?to=host-left') {
json(response, {
ok: false,
error: { code: 'connect_unreachable', message: 'host unavailable' },
});
pending ? 409 : 504
);
}
});

const joined = await runCli(home, ['join', 'connect-joined', '--json']);
expect(joined.code).toBe(0);
expect(JSON.parse(joined.stdout).data.connect_id).toBe('connect-joined');
expect(joined.stderr).toBe(
'Joined, but could not notify the host: Relay Connect cannot reach Cloud or Relaycast; retry once.\n'
);
const joined = await runCli(home, ['join', 'connect-timed-out']);
expect(joined).toEqual({
code: 8,
stdout: '',
stderr: 'Relay Connect join timed out; retry the same join safely.\n',
});
const pending = await runCli(home, ['join', 'connect-pending']);
expect(pending).toEqual({
code: 8,
stdout: '',
stderr: 'A previous join has an unknown outcome; retry with the same link and options.\n',
});
});

it('maps socket errors and preserves their code in JSON mode', async () => {
Expand Down
Loading