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 +1 -1
- package/src/cli.ts +115 -1
- package/src/upgrade.ts +51 -6
package/package.json
CHANGED
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) =>
|
|
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
|
|
305
|
-
if (
|
|
306
|
-
throw new Error(`agent-relay-orchestrator.service restarted but ${
|
|
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
|
}
|