Pong.expires at pong.proto:14 carries the connected Node's decommission timestamp as self-reported by the Node. Pong.properties[i].expires at pong.proto:7 (via the Property type) carries the same timestamp for the same Node as recorded by the Hub. When the client holds a Property slot backed by the currently-connected Node, both fields describe the same physical event — the Node's retirement — but from different data paths.
The Hub populates Property.expires from its fleet registry; the Node writes Pong.expires from its own configuration. These two sources can disagree during a rolling decommission, an emergency retirement, or any window where Hub and Node records are temporarily out of sync. The protocol defines no tiebreaker: a client receiving Pong.expires = T₁ and the matching Property.expires = T₂ where T₁ ≠ T₂ has no rule to follow. Using the earlier value causes premature migration to a peer; using the later value delays migration past the Node's actual retirement, causing the tunnel to drop when the Node disappears.
Remove Pong.expires and reserve field 14; make Property.expires on the matching Property the single authoritative source for the connected Node's retirement time. If field 14 must be retained for wire-compatibility, add a comment declaring explicitly that Pong.expires takes precedence over Property.expires when both are present and non-zero.
Pong.expiresatpong.proto:14carries the connected Node's decommission timestamp as self-reported by the Node.Pong.properties[i].expiresatpong.proto:7(via thePropertytype) carries the same timestamp for the same Node as recorded by the Hub. When the client holds a Property slot backed by the currently-connected Node, both fields describe the same physical event — the Node's retirement — but from different data paths.The Hub populates
Property.expiresfrom its fleet registry; the Node writesPong.expiresfrom its own configuration. These two sources can disagree during a rolling decommission, an emergency retirement, or any window where Hub and Node records are temporarily out of sync. The protocol defines no tiebreaker: a client receivingPong.expires = T₁and the matchingProperty.expires = T₂whereT₁ ≠ T₂has no rule to follow. Using the earlier value causes premature migration to a peer; using the later value delays migration past the Node's actual retirement, causing the tunnel to drop when the Node disappears.Remove
Pong.expiresand reserve field 14; makeProperty.expireson the matching Property the single authoritative source for the connected Node's retirement time. If field 14 must be retained for wire-compatibility, add a comment declaring explicitly thatPong.expirestakes precedence overProperty.expireswhen both are present and non-zero.