Skip to content

Commit 8fd21a0

Browse files
khaliqgantProactive Runtime Botclaude
authored
feat(cli): add --broker-name override to agent-relay up (#939)
The broker name is otherwise derived from the project directory basename, which means re-running `up` against a workspace that already has an agent with that name fails the relaycast handshake with `agent name '<name>' already exists`. The only existing workarounds were to rename the project directory or rotate the workspace key. Threads a `brokerName` option through `UpOptions`, `CoreDependencies.createRelay`, `createDefaultRelay`, and `createAgentRelayClient` into the SDK's `AgentRelayClient.spawn`, which already supported it. Also forwards `--broker-name` from background re-execs in `childUpArgsForDetachedStart` so programmatic callers (not just CLI users whose argv is preserved verbatim) get the override. Co-authored-by: Proactive Runtime Bot <agent@agent-relay.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6de2b30 commit 8fd21a0

4 files changed

Lines changed: 32 additions & 9 deletions

File tree

src/cli/commands/core.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ describe('registerCoreCommands', () => {
205205
);
206206
});
207207

208+
it('up forwards --broker-name to createRelay', async () => {
209+
const relay = createRelayMock({
210+
getStatus: vi.fn(async () => ({ agent_count: 1, pending_delivery_count: 0 })),
211+
});
212+
const { program, deps } = createHarness({ relay });
213+
214+
const exitCode = await runCommand(program, ['up', '--port', '4999', '--broker-name', 'relayfile-dev']);
215+
216+
expect(exitCode).toBeUndefined();
217+
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000, 'relayfile-dev');
218+
});
219+
208220
it('up starts broker and dashboard process', async () => {
209221
const relay = createRelayMock({
210222
getStatus: vi.fn(async () => ({ agent_count: 1, pending_delivery_count: 0 })),
@@ -214,7 +226,7 @@ describe('registerCoreCommands', () => {
214226
const exitCode = await runCommand(program, ['up', '--port', '4999']);
215227

216228
expect(exitCode).toBeUndefined();
217-
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000);
229+
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000, undefined);
218230
expect(deps.spawnProcess).toHaveBeenCalledWith(
219231
'/usr/local/bin/relay-dashboard-server',
220232
expect.arrayContaining(['--port', '4999', '--relay-url', 'http://127.0.0.1:5000']),
@@ -236,7 +248,7 @@ describe('registerCoreCommands', () => {
236248
const exitCode = await runCommand(program, ['start', 'dashboard.js', 'claude', '--port', '4999']);
237249

238250
expect(exitCode).toBeUndefined();
239-
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000);
251+
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000, undefined);
240252
expect(deps.spawnProcess).toHaveBeenCalledWith(
241253
'/usr/local/bin/relay-dashboard-server',
242254
expect.arrayContaining(['--port', '4999', '--relay-url', 'http://127.0.0.1:5000']),
@@ -445,7 +457,7 @@ describe('registerCoreCommands', () => {
445457
// Port probing happens before createRelay — only one broker is spawned
446458
expect(deps.createRelay).toHaveBeenCalledTimes(1);
447459
// API port = dashboard port (3888) + 1 = 3889
448-
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889);
460+
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889, undefined);
449461
expect(relay.getStatus).toHaveBeenCalledTimes(1);
450462
});
451463

@@ -457,7 +469,7 @@ describe('registerCoreCommands', () => {
457469

458470
expect(exitCode).toBeUndefined();
459471
expect(deps.createRelay).toHaveBeenCalledTimes(1);
460-
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889);
472+
expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889, undefined);
461473
expect(relay.getStatus).toHaveBeenCalledTimes(1);
462474
});
463475

src/cli/commands/core.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface CoreDependencies {
105105
missing: BridgeProject[];
106106
};
107107
getAgentOutboxTemplate: () => string;
108-
createRelay: (cwd: string, apiPort?: number) => CoreRelay | Promise<CoreRelay>;
108+
createRelay: (cwd: string, apiPort?: number, brokerName?: string) => CoreRelay | Promise<CoreRelay>;
109109
findDashboardBinary: () => string | null;
110110
spawnProcess: (command: string, args: string[], options?: Record<string, unknown>) => SpawnedProcess;
111111
execCommand: (command: string) => Promise<{ stdout: string; stderr: string }>;
@@ -245,7 +245,7 @@ function findDashboardBinaryDefault(fileSystem: CoreFileSystem): string | null {
245245
return null;
246246
}
247247

248-
async function createDefaultRelay(cwd: string, apiPort = 0): Promise<CoreRelay> {
248+
async function createDefaultRelay(cwd: string, apiPort = 0, brokerName?: string): Promise<CoreRelay> {
249249
const binaryArgs: AgentRelayBrokerInitArgs = {};
250250
if (apiPort > 0) {
251251
binaryArgs.persist = true;
@@ -258,6 +258,7 @@ async function createDefaultRelay(cwd: string, apiPort = 0): Promise<CoreRelay>
258258
const client = await createAgentRelayClient({
259259
cwd,
260260
binaryArgs,
261+
brokerName,
261262
preferConnect: apiPort > 0,
262263
});
263264

@@ -415,6 +416,7 @@ export function registerCoreCommands(program: Command, overrides: Partial<CoreDe
415416
.option('--verbose', 'Enable verbose logging')
416417
.option('--workspace-key <key>', 'Use a pre-established Relaycast workspace key')
417418
.option('--state-dir <path>', 'Directory for broker state and connection files (default: .agent-relay/)')
419+
.option('--broker-name <name>', 'Override the broker name (defaults to project directory basename)')
418420
.action(
419421
async (options: {
420422
dashboard?: boolean;
@@ -425,6 +427,7 @@ export function registerCoreCommands(program: Command, overrides: Partial<CoreDe
425427
verbose?: boolean;
426428
workspaceKey?: string;
427429
stateDir?: string;
430+
brokerName?: string;
428431
}) => {
429432
await runUpCommand(options, deps);
430433
}

src/cli/lib/broker-lifecycle.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type UpOptions = {
1919
reuseExistingBroker?: boolean;
2020
workspaceKey?: string;
2121
stateDir?: string;
22+
brokerName?: string;
2223
};
2324

2425
type DownOptions = {
@@ -209,15 +210,16 @@ async function resolveApiPortWithFallback(
209210
async function startBrokerWithPortFallback(
210211
paths: CoreProjectPaths,
211212
dashboardPort: number,
212-
deps: CoreDependencies
213+
deps: CoreDependencies,
214+
brokerName?: string
213215
): Promise<{ relay: CoreRelay; apiPort: number }> {
214216
// Resolve a free API port BEFORE spawning the broker. This avoids
215217
// spawning (and flocking) multiple --persist brokers during retry,
216218
// which caused stale-flock "already running" errors.
217219
const startApiPort = dashboardPort + 1;
218220
const apiPort = await resolveApiPortWithFallback(startApiPort, MAX_API_PORT_ATTEMPTS, deps);
219221

220-
const candidate = await deps.createRelay(paths.projectRoot, apiPort);
222+
const candidate = await deps.createRelay(paths.projectRoot, apiPort, brokerName);
221223

222224
await candidate.getStatus();
223225
return { relay: candidate, apiPort };
@@ -559,6 +561,9 @@ function childUpArgsForDetachedStart(options: UpOptions, deps: CoreDependencies)
559561
if (options.workspaceKey && !hasCliOption(args, '--workspace-key')) {
560562
args.push('--workspace-key', options.workspaceKey);
561563
}
564+
if (options.brokerName && !hasCliOption(args, '--broker-name')) {
565+
args.push('--broker-name', options.brokerName);
566+
}
562567
if (options.verbose === true && !args.includes('--verbose')) {
563568
args.push('--verbose');
564569
}
@@ -1463,7 +1468,7 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies):
14631468
// files (e.g. user deleted .agent-relay/ while broker was running).
14641469
await killOrphanedBrokerProcesses(paths.projectRoot, deps);
14651470

1466-
const started = await startBrokerWithPortFallback(paths, dashboardPort, deps);
1471+
const started = await startBrokerWithPortFallback(paths, dashboardPort, deps, options.brokerName);
14671472
relay = started.relay;
14681473
apiPort = started.apiPort;
14691474
const dashboardRelayUrl = resolveDashboardRelayUrl(apiPort, deps);

src/cli/lib/client-factory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface CreateAgentRelayClientOptions {
55
channels?: string[];
66
binaryPath?: string;
77
binaryArgs?: AgentRelayBrokerInitArgs;
8+
brokerName?: string;
89
env?: NodeJS.ProcessEnv;
910
preferConnect?: boolean;
1011
}
@@ -30,6 +31,7 @@ export async function createAgentRelayClient(
3031
channels = ['general'],
3132
binaryPath = process.env.AGENT_RELAY_BIN,
3233
binaryArgs,
34+
brokerName,
3335
env = process.env,
3436
preferConnect = false,
3537
} = options;
@@ -45,6 +47,7 @@ export async function createAgentRelayClient(
4547
return AgentRelayClient.spawn({
4648
binaryPath: binaryPath || undefined,
4749
binaryArgs,
50+
brokerName,
4851
channels,
4952
cwd,
5053
env: env as Record<string, string>,

0 commit comments

Comments
 (0)