agent-relay-server 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -39,13 +39,14 @@ import {
39
39
  detectUpgradeSnapshot,
40
40
  executeUpgradePlan,
41
41
  formatUpgradePlan,
42
+ resolveLocalOrchestratorId,
42
43
  type UpgradeProvider,
43
44
  } from "./upgrade";
44
45
  import { formatMemoryBrokerSmokeResult, runMemoryBrokerSmoke } from "./memory-broker-smoke";
45
46
  import { MAX_BODY_BYTES, VERSION } from "./config";
46
47
  import { DEFAULT_CONTEXT_PROBE_STATE_DIR, runContextProbe } from "agent-relay-sdk/context-probe";
47
48
  import { shellQuote } from "agent-relay-sdk/shell-utils";
48
- import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
49
+ import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
49
50
  import type { WorkspaceDepsRefreshResult } from "agent-relay-sdk";
50
51
 
51
52
  const HELP = `
@@ -55,6 +56,8 @@ Usage:
55
56
  agent-relay [start]
56
57
  agent-relay setup [--yes] [--dry-run] [--force] [--env-file PATH] [--runtime-prefix DIR] [--host HOST] [--port PORT] [--db-path PATH] [--token TOKEN|--no-token]
57
58
  agent-relay upgrade [--dry-run] [--version VERSION] [--runtime-prefix DIR] [--providers auto|all|codex|claude|orchestrator] [--no-restart] [--yes]
59
+ agent-relay upgrade --host ID [--host ID2 ...] [--version VERSION] [--providers ...] (upgrade remote orchestrator hosts over the relay)
60
+ agent-relay upgrade --all-hosts [...] (upgrade this host, then every behind remote host)
58
61
  agent-relay setup upgrade [same options as upgrade]
59
62
  agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]
60
63
  agent-relay orchestrator install [options]
@@ -135,6 +138,8 @@ Upgrade options:
135
138
  --version VERSION Target version (default: latest published server version)
136
139
  --runtime-prefix DIR Isolated Agent Relay npm prefix (default: ~/.agent-relay/runtime)
137
140
  --providers LIST Provider integrations to upgrade: auto, all, codex, claude, orchestrator
141
+ --host ID Upgrade a remote orchestrator host over the relay (repeatable). Skips the local upgrade
142
+ --all-hosts Upgrade this host, then fan out to every connected remote host that is behind
138
143
  --no-restart Do not restart agent-relay.service
139
144
  --dry-run Print detected state and planned commands
140
145
  --yes Skip confirmation prompts
@@ -391,6 +396,8 @@ async function handleUpgradeCommand(args: string[]): Promise<void> {
391
396
  let runtimePrefix: string | undefined;
392
397
  const pathPrefix: string[] = [];
393
398
  const providers: UpgradeProvider[] = [];
399
+ const hosts: string[] = [];
400
+ let allHosts = false;
394
401
 
395
402
  for (let i = 0; i < args.length; i++) {
396
403
  const arg = args[i];
@@ -399,6 +406,8 @@ async function handleUpgradeCommand(args: string[]): Promise<void> {
399
406
  else if (arg === "--providers" && i + 1 < args.length) {
400
407
  for (const provider of args[++i]!.split(",")) providers.push(parseUpgradeProvider(provider));
401
408
  } else if (arg === "--provider" && i + 1 < args.length) providers.push(parseUpgradeProvider(args[++i]!));
409
+ else if (arg === "--host" && i + 1 < args.length) hosts.push(args[++i]!);
410
+ else if (arg === "--all-hosts") allHosts = true;
402
411
  else if (arg === "--codex") providers.push("codex");
403
412
  else if (arg === "--claude") providers.push("claude");
404
413
  else if (arg === "--orchestrator") providers.push("orchestrator");
@@ -410,6 +419,14 @@ async function handleUpgradeCommand(args: string[]): Promise<void> {
410
419
  else throw new Error(`Unknown upgrade option "${arg}"`);
411
420
  }
412
421
 
422
+ // Remote-only: drive named hosts' self-upgrade over the relay command bus and
423
+ // skip the local upgrade entirely (#210). `--all-hosts` instead upgrades this
424
+ // host first, then fans out to every behind remote (handled after the local run).
425
+ if (hosts.length && !allHosts) {
426
+ await runRemoteOrchestratorUpgrades({ hosts, targetVersion, providers, json, dryRun, yes });
427
+ return;
428
+ }
429
+
413
430
  const snapshot = await detectUpgradeSnapshot({
414
431
  ...(targetVersion ? { targetVersion } : {}),
415
432
  ...(runtimePrefix ? { runtimePrefix } : {}),
@@ -425,11 +442,13 @@ async function handleUpgradeCommand(args: string[]): Promise<void> {
425
442
 
426
443
  if (json) {
427
444
  console.log(JSON.stringify({ plan }, null, 2));
445
+ if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, dryRun: true });
428
446
  return;
429
447
  }
430
448
 
431
449
  if (dryRun) {
432
450
  console.log(formatUpgradePlan(plan, { dryRun: true }));
451
+ if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, dryRun: true });
433
452
  return;
434
453
  }
435
454
 
@@ -443,6 +462,101 @@ async function handleUpgradeCommand(args: string[]): Promise<void> {
443
462
  }
444
463
 
445
464
  console.log(await executeUpgradePlan(plan));
465
+
466
+ if (allHosts) {
467
+ await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, yes: true });
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Trigger orchestrator self-upgrade on remote hosts via the relay command bus
473
+ * (#210). Each host installs its own runtime + self-restarts; the relay settles
474
+ * the version when it re-registers. Replaces the manual ssh + npm install dance.
475
+ */
476
+ async function runRemoteOrchestratorUpgrades(opts: {
477
+ hosts?: string[];
478
+ allBehind?: boolean;
479
+ targetVersion?: string;
480
+ providers: UpgradeProvider[];
481
+ json?: boolean;
482
+ dryRun?: boolean;
483
+ yes?: boolean;
484
+ }): Promise<void> {
485
+ const targetVersion = opts.targetVersion ?? VERSION;
486
+ const orchestrators = (await apiRequest("GET", "/api/orchestrators")) as Array<{
487
+ id: string;
488
+ version?: string;
489
+ }>;
490
+ const byId = new Map(orchestrators.map((orch) => [orch.id, orch]));
491
+ const localId = resolveLocalOrchestratorId();
492
+ // Default to "all" so a remote host's provider runner is upgraded too, not just
493
+ // the orchestrator package (matters for hosts running claude/codex agents).
494
+ const remoteProviders: UpgradeProvider[] = opts.providers.length ? opts.providers : ["all"];
495
+
496
+ let targets: string[];
497
+ if (opts.allBehind) {
498
+ targets = orchestrators
499
+ .filter((orch) => orch.id !== localId && orch.version && orch.version !== targetVersion)
500
+ .map((orch) => orch.id);
501
+ if (!targets.length) {
502
+ if (opts.json) console.log(JSON.stringify({ remoteUpgrades: [] }, null, 2));
503
+ else console.log(`No remote orchestrators behind ${targetVersion}.`);
504
+ return;
505
+ }
506
+ } else {
507
+ targets = opts.hosts ?? [];
508
+ }
509
+
510
+ if (opts.dryRun) {
511
+ const lines = targets.map((id) => {
512
+ const orch = byId.get(id);
513
+ const from = orch ? orch.version ?? "unknown" : "(not connected)";
514
+ return ` ${id}: ${from} → ${targetVersion} (providers: ${remoteProviders.join(",")})`;
515
+ });
516
+ if (opts.json) console.log(JSON.stringify({ remoteUpgrades: targets.map((id) => ({ id, targetVersion, providers: remoteProviders, dryRun: true })) }, null, 2));
517
+ else console.log(`Remote orchestrator upgrade plan → ${targetVersion}:\n${lines.join("\n")}`);
518
+ return;
519
+ }
520
+
521
+ if (!opts.yes && !opts.json) {
522
+ console.log(`Trigger remote orchestrator upgrade → ${targetVersion} for: ${targets.join(", ")}`);
523
+ const ok = await confirm("Send remote upgrade command(s)?");
524
+ if (!ok) {
525
+ console.log("Remote upgrade cancelled.");
526
+ return;
527
+ }
528
+ }
529
+
530
+ const results: Array<{ id: string; ok: boolean; message: string }> = [];
531
+ for (const id of targets) {
532
+ if (!byId.has(id)) {
533
+ results.push({ id, ok: false, message: "not connected to the relay" });
534
+ continue;
535
+ }
536
+ try {
537
+ const res = (await apiRequest("POST", `/api/orchestrators/${encodeURIComponent(id)}/action`, {
538
+ action: "upgrade",
539
+ targetVersion,
540
+ providers: remoteProviders,
541
+ })) as { command?: { id?: string } };
542
+ const from = byId.get(id)?.version;
543
+ results.push({ id, ok: true, message: `queued ${from ?? "?"} → ${targetVersion} (command ${res?.command?.id ?? "?"})` });
544
+ } catch (err) {
545
+ results.push({ id, ok: false, message: errMessage(err) });
546
+ }
547
+ }
548
+
549
+ if (opts.json) {
550
+ console.log(JSON.stringify({ remoteUpgrades: results }, null, 2));
551
+ return;
552
+ }
553
+ console.log(`\nRemote orchestrator upgrades → ${targetVersion}:`);
554
+ for (const result of results) console.log(` ${result.ok ? "✓" : "✗"} ${result.id}: ${result.message}`);
555
+ console.log("\nEach host installs and self-restarts; the relay reconciles the version when it re-registers.");
556
+ console.log("Track progress in the dashboard Orchestrators view or via GET /api/orchestrators.");
557
+ if (results.some((result) => !result.ok)) {
558
+ process.exitCode = 1;
559
+ }
446
560
  }
447
561
 
448
562
  function parseUpgradeProvider(value: string): UpgradeProvider {
package/src/upgrade.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
2
+ import { homedir, hostname as osHostname } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { VERSION } from "./config";
5
5
  import type { RuntimeContracts, RuntimePackageMetadata } from "./contracts";
@@ -256,6 +256,8 @@ export type ExecuteUpgradeOptions = {
256
256
  sleep?: (ms: number) => Promise<void>;
257
257
  probeServerVersion?: () => Promise<string | undefined>;
258
258
  probeOrchestrators?: () => Promise<UpgradeSnapshot["runningOrchestrators"]>;
259
+ /** Override the local orchestrator id (defaults to the resolved host id). */
260
+ localOrchestratorId?: string;
259
261
  };
260
262
 
261
263
  export async function executeUpgradePlan(plan: UpgradePlan, options: ExecuteUpgradeOptions = {}): Promise<string> {
@@ -297,15 +299,35 @@ export async function executeUpgradePlan(plan: UpgradePlan, options: ExecuteUpgr
297
299
  if (serverVersion) lines.push(`Running server: ${serverVersion}`);
298
300
  }
299
301
  if (plan.actions.some((action) => action.command.join(" ") === "systemctl --user restart agent-relay-orchestrator.service")) {
302
+ // This command only upgrades the LOCAL runtime. Remote hosts run their own
303
+ // orchestrator runtime and upgrade separately, so the success check asserts
304
+ // the local orchestrator only — a behind remote host is advisory, never a
305
+ // failure (#210). Wait for the local one to re-register at the target.
306
+ const localId = options.localOrchestratorId ?? resolveLocalOrchestratorId();
300
307
  const orchestrators = await pollUntil(
301
308
  async () => (await probeOrchestrators()) ?? [],
302
- (list) => list.length > 0 && list.every((orch) => !orch.version || orch.version === plan.targetVersion),
309
+ (list) => {
310
+ const local = list.find((orch) => orch.id === localId);
311
+ return Boolean(local && (!local.version || local.version === plan.targetVersion));
312
+ },
303
313
  );
304
- const mismatched = orchestrators.filter((orch) => orch.version && orch.version !== plan.targetVersion);
305
- if (mismatched.length > 0) {
306
- throw new Error(`agent-relay-orchestrator.service restarted but ${mismatched.map((orch) => `${orch.id} reports ${orch.version}`).join(", ")}, expected ${plan.targetVersion}`);
314
+ const local = orchestrators.find((orch) => orch.id === localId);
315
+ if (local?.version && local.version !== plan.targetVersion) {
316
+ throw new Error(`agent-relay-orchestrator.service restarted but ${local.id} reports ${local.version}, expected ${plan.targetVersion}`);
317
+ }
318
+ if (local) {
319
+ lines.push(`Running orchestrator: ${local.id}${local.version ? ` ${local.version}` : ""}`);
320
+ } else {
321
+ lines.push(`Local orchestrator ${localId} has not re-registered with the relay yet; it should appear shortly.`);
322
+ }
323
+ const remoteBehind = orchestrators.filter((orch) => orch.id !== localId && orch.version && orch.version !== plan.targetVersion);
324
+ if (remoteBehind.length > 0) {
325
+ lines.push("");
326
+ lines.push(`Remote orchestrator(s) behind ${plan.targetVersion} (each host upgrades its own runtime):`);
327
+ for (const orch of remoteBehind) {
328
+ lines.push(`- ${orch.id} ${orch.version} → upgrade with: agent-relay upgrade --host ${orch.id}`);
329
+ }
307
330
  }
308
- if (orchestrators.length > 0) lines.push(`Running orchestrator(s): ${orchestrators.map((orch) => `${orch.id}${orch.version ? ` ${orch.version}` : ""}`).join(", ")}`);
309
331
  }
310
332
  lines.push("\nUpgrade commands completed.");
311
333
  if (plan.warnings.length > 0) {
@@ -534,6 +556,29 @@ function homeDir(): string {
534
556
  return process.env.HOME || process.env.USERPROFILE || homedir();
535
557
  }
536
558
 
559
+ /**
560
+ * Resolve the LOCAL host's orchestrator id the same way the orchestrator runtime
561
+ * does (orchestrator/src/config.ts): explicit config id, then
562
+ * AGENT_RELAY_ORCHESTRATOR_ID, then the hostname with dots hyphenated. The
563
+ * upgrade command only ever touches the local runtime, so this is the only
564
+ * orchestrator whose post-restart version it can legitimately assert (#210).
565
+ */
566
+ export function resolveLocalOrchestratorId(homeDirOverride?: string): string {
567
+ const home = homeDirOverride ?? homeDir();
568
+ const configPath = join(home, ".agent-relay", "orchestrator.json");
569
+ if (existsSync(configPath)) {
570
+ try {
571
+ const parsed = JSON.parse(readFileSync(configPath, "utf8")) as { id?: string };
572
+ if (typeof parsed.id === "string" && parsed.id.trim()) return parsed.id.trim();
573
+ } catch {
574
+ // Unparsable config — fall through to env/hostname.
575
+ }
576
+ }
577
+ const envId = process.env.AGENT_RELAY_ORCHESTRATOR_ID?.trim();
578
+ if (envId) return envId;
579
+ return osHostname().replace(/\./g, "-");
580
+ }
581
+
537
582
  function uniqueStrings(values: string[]): string[] {
538
583
  return [...new Set(values.filter(Boolean))];
539
584
  }