agent-relay-runner 0.64.0 → 0.64.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-runner",
3
- "version": "0.64.0",
3
+ "version": "0.64.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.64.0",
4
+ "version": "0.64.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/config.ts CHANGED
@@ -89,6 +89,16 @@ export function runtimeTokenExpiresAtFromEnv(): string | undefined {
89
89
  return process.env.AGENT_RELAY_TOKEN_EXPIRES_AT;
90
90
  }
91
91
 
92
+ // Deadline for a freshly spawned runner's INITIAL relay registration. Reconnects after the
93
+ // first registration retry forever (a long-lived agent rides out a relay blip), but the
94
+ // initial handshake is bounded: a runner that can never register (a token the relay rejects,
95
+ // an unreachable relay) must EXIT instead of hanging `active` forever — a hung runner is
96
+ // invisible to the orchestrator's exit detection, so the spawn-parent is never notified.
97
+ export function registrationTimeoutMsFromEnv(): number {
98
+ const parsed = Number(process.env.AGENT_RELAY_REGISTRATION_TIMEOUT_MS);
99
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;
100
+ }
101
+
92
102
  export function agentProfileNameFromEnv(): string | undefined {
93
103
  return process.env.AGENT_RELAY_AGENT_PROFILE;
94
104
  }
package/src/index.ts CHANGED
@@ -344,5 +344,11 @@ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string
344
344
  }
345
345
 
346
346
  if (import.meta.main) {
347
- await main();
347
+ main().catch((error) => {
348
+ // A fatal startup failure (e.g. the runner could not register with the relay within the
349
+ // deadline) must exit non-zero, not hang or swallow — the orchestrator's exit detection
350
+ // then reports it and the spawn-parent is notified instead of waiting on a silent zombie.
351
+ console.error(`[agent-relay] runner fatal: ${errMessage(error)}`);
352
+ process.exit(1);
353
+ });
348
354
  }
@@ -22,7 +22,7 @@ import { ensureSessionScratch, reapSessionScratch, sweepStaleSessions, type Sess
22
22
  import { deliverBufferedMcpCall as deliverBufferedMcpOutboxCall, deliverRunnerOutboxEvent } from "./mcp-outbox";
23
23
  import { RunnerInsights } from "./runner-insights";
24
24
  import { BusyReconciler } from "./busy-reconciler";
25
- import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
25
+ import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
26
26
  import {
27
27
  appliedAgentProfileMetadata,
28
28
  commandTimeoutMs,
@@ -34,6 +34,7 @@ import {
34
34
  latestClaudeResumeIdFromLogFile,
35
35
  lifecycleCapabilities,
36
36
  providerStateFromActiveWork,
37
+ registerWithinDeadline,
37
38
  relayBusUrl,
38
39
  runnerAgentStatus,
39
40
  runnerBusErrorAction,
@@ -447,7 +448,7 @@ export class AgentRunner {
447
448
  void this.handleCommand(type, params, commandId, command);
448
449
  });
449
450
  this.bus.on("error", (code, message) => this.handleBusError(String(code), String(message)));
450
- await this.bus.connect();
451
+ await registerWithinDeadline(this.bus, registrationTimeoutMsFromEnv(), this.options.relayUrl, (reason) => logger.fatal("register", reason));
451
452
  this.obligationCache.start();
452
453
  this.outbox.start();
453
454
  this.sessionOutbox.start();
@@ -56,6 +56,36 @@ export function stripRunnerClaimedGuidance(body: string): string {
56
56
  return body.replace(RUNNER_CLAIMED_GUIDANCE_RE, "");
57
57
  }
58
58
 
59
+ // Bound a runner's INITIAL relay registration. `connect` resolves on the first `registered`
60
+ // frame and otherwise loops forever, so a token the relay rejects (401 at the WS upgrade)
61
+ // hangs here silently. Race it against `timeoutMs`; on timeout, surface a fatal reason, tear
62
+ // down the bus (stop the retry loop) and throw so the process exits and the orchestrator
63
+ // observes it instead of a silent zombie. Reconnects after first registration stay infinite.
64
+ export async function registerWithinDeadline(
65
+ bus: { connect: () => Promise<void>; close: () => Promise<void> },
66
+ timeoutMs: number,
67
+ relayUrl: string,
68
+ onFatal: (reason: string) => void,
69
+ ): Promise<void> {
70
+ let timer: ReturnType<typeof setTimeout> | undefined;
71
+ const deadline = new Promise<never>((_, reject) => {
72
+ timer = setTimeout(() => reject(new Error(
73
+ `runner failed to register with the relay within ${timeoutMs}ms — aborting so the orchestrator `
74
+ + `observes the exit instead of a silent hang (relay ${relayUrl}; check the relay is reachable `
75
+ + `and the runner token is valid).`,
76
+ )), timeoutMs);
77
+ });
78
+ try {
79
+ await Promise.race([bus.connect(), deadline]);
80
+ } catch (error) {
81
+ onFatal(errMessage(error));
82
+ await bus.close().catch(() => {});
83
+ throw error;
84
+ } finally {
85
+ if (timer) clearTimeout(timer);
86
+ }
87
+ }
88
+
59
89
  export function csvTags(raw: string | undefined): string[] {
60
90
  return (raw || "").split(",").map((tag) => tag.trim()).filter(Boolean);
61
91
  }