@bivy/bivy 0.10.1-staging.366 → 0.10.1-staging.367

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.
@@ -6,7 +6,16 @@ import { WebSocket } from "ws";
6
6
  import { seal, sealFrame, openFrame, ReplayGuard } from "../e2e.js";
7
7
  import { frameMessages, FrameReassembler } from "../relay-chunk.js";
8
8
  import { soloCredentials, buildDialUrl } from "./solo.js";
9
- function isFatalRelayError(message) {
9
+ // Relay admission/config rejections: the relay refused this node at connect time
10
+ // (bad/expired ticket, relay disabled, missing token). Crucially a routine
11
+ // control-plane/relay DEPLOY produces exactly these for a few seconds — a
12
+ // reconnecting node races the still-restarting control plane, its ticket
13
+ // introspection transiently fails, and the relay reports "Unauthorized node". So
14
+ // this is NOT permanently fatal: we keep retrying (on the slower admission
15
+ // ceiling below) and reconnect on our own once admission passes again.
16
+ // Previously these disabled the connector for good, so every routine update
17
+ // knocked every node offline until each was restarted by hand.
18
+ function isAdmissionRelayError(message) {
10
19
  return /hosted relay is not enabled|unauthorized|missing token/i.test(message);
11
20
  }
12
21
  export function loadRelayConfig(appDir) {
@@ -50,12 +59,27 @@ const HEARTBEAT_TIMEOUT_MS = 75_000;
50
59
  // rate-limited node reconnect every ~1s indefinitely — a storm that itself keeps
51
60
  // tripping the limiter. Requiring a stable window lets backoff escalate normally.
52
61
  const BACKOFF_STABLE_RESET_MS = 30_000;
62
+ // Reconnect backoff ceiling for a transient network drop.
63
+ const BACKOFF_MAX_MS = 30_000;
64
+ // Reconnect backoff ceiling once the relay has actively REJECTED admission (see
65
+ // isAdmissionRelayError). Slower than the network ceiling so a node that is
66
+ // genuinely deauthorized (removed, plan lost, relay turned off) retries gently
67
+ // instead of hammering the relay every 30s forever — while still recovering
68
+ // automatically, within this bound, the moment admission is restored (deploy
69
+ // finished, node re-authorized). No manual node restart required either way.
70
+ const ADMISSION_BACKOFF_MAX_MS = 5 * 60_000;
53
71
  export class RelayConnector {
54
72
  config;
55
73
  onClientMessage;
56
74
  ws;
57
75
  closed = false;
58
76
  backoff = 1000;
77
+ // Set when the relay actively rejected admission (isAdmissionRelayError).
78
+ // Raises the reconnect ceiling to ADMISSION_BACKOFF_MAX_MS so we retry gently
79
+ // rather than give up; cleared once we reconnect. Deliberately NOT `closed` —
80
+ // an admission rejection must never permanently stop the connector, or a
81
+ // routine deploy takes the node offline until someone restarts it by hand.
82
+ admissionRejected = false;
59
83
  heartbeatTimer;
60
84
  stableTimer;
61
85
  lastPongAt = 0;
@@ -102,6 +126,7 @@ export class RelayConnector {
102
126
  }
103
127
  start() {
104
128
  this.closed = false;
129
+ this.admissionRejected = false;
105
130
  void this.connect();
106
131
  }
107
132
  stop() {
@@ -315,6 +340,9 @@ export class RelayConnector {
315
340
  if (env.t === "ready") {
316
341
  this.ready = true;
317
342
  this.lastErrorMessage = undefined;
343
+ // Admission passed — drop back to the fast network ceiling for any
344
+ // future drop (scheduleBackoffReset restores the 1s floor once stable).
345
+ this.admissionRejected = false;
318
346
  this.startHeartbeat(ws);
319
347
  this.scheduleBackoffReset(ws);
320
348
  console.log("[relay] connected");
@@ -352,9 +380,14 @@ export class RelayConnector {
352
380
  const message = env.error || "Relay error";
353
381
  this.lastErrorMessage = message;
354
382
  console.warn("[relay] error:", message);
355
- if (isFatalRelayError(message)) {
356
- console.warn("[relay] disabling connector; fix relay setup/plan and restart the node dev server");
357
- this.closed = true;
383
+ if (isAdmissionRelayError(message)) {
384
+ // Do NOT disable the connector: a deploy rejects a reconnecting node
385
+ // this way for a few seconds, and permanently stopping meant a routine
386
+ // update took the node offline until it was manually restarted. Raise
387
+ // the backoff ceiling and let the close handler reconnect us; we
388
+ // recover on our own once admission passes again.
389
+ this.admissionRejected = true;
390
+ console.warn("[relay] admission rejected; retrying on a slow backoff (auto-recovers, no restart needed)");
358
391
  ws.close();
359
392
  }
360
393
  }
@@ -376,8 +409,12 @@ export class RelayConnector {
376
409
  scheduleReconnect() {
377
410
  if (this.closed)
378
411
  return;
379
- const wait = this.backoff;
380
- this.backoff = Math.min(this.backoff * 2, 30_000);
412
+ // A relay that actively rejected admission retries on a much slower ceiling
413
+ // than a transient network drop, so a genuinely-unauthorized node doesn't
414
+ // hammer the relay while still auto-recovering when admission is restored.
415
+ const cap = this.admissionRejected ? ADMISSION_BACKOFF_MAX_MS : BACKOFF_MAX_MS;
416
+ const wait = Math.min(this.backoff, cap);
417
+ this.backoff = Math.min(this.backoff * 2, cap);
381
418
  setTimeout(() => {
382
419
  if (!this.closed)
383
420
  void this.connect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.366",
3
+ "version": "0.10.1-staging.367",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",