@@ -135,6 +135,7 @@ class NodeService {
135135 /// Resolve the gateway auth token from available sources:
136136 /// 1. Manually entered token (for remote gateways)
137137 /// 2. Dashboard URL fragment (for local gateway)
138+ /// 3. openclaw.json config file (source of truth — fallback when URL is stale or missing)
138139 Future <String ?> _readGatewayToken () async {
139140 final prefs = PreferencesService ();
140141 await prefs.init ();
@@ -156,6 +157,24 @@ class NodeService {
156157 }
157158 }
158159
160+ // 3. Read directly from openclaw.json — the source of truth (#94).
161+ // This catches the case where dashboardUrl was cleared before a gateway
162+ // restart (GatewayService.start() nulls it out) but the config file still
163+ // holds the authoritative token, preventing token_missing reconnect loops.
164+ try {
165+ final raw = await NativeBridge .readRootfsFile ('root/.openclaw/openclaw.json' );
166+ if (raw != null ) {
167+ final config = jsonDecode (raw) as Map <String , dynamic >;
168+ final token = config['gateway' ]? ['auth' ]? ['token' ];
169+ if (token is String && token.isNotEmpty) {
170+ _log ('[NODE] Gateway token read from openclaw.json config' );
171+ return token;
172+ }
173+ }
174+ } catch (e) {
175+ _log ('[NODE] Could not read token from openclaw.json: $e ' );
176+ }
177+
159178 _log ('[NODE] No gateway token available' );
160179 return null ;
161180 }
@@ -256,6 +275,9 @@ class NodeService {
256275 }
257276
258277 void _onConnected (NodeFrame frame) {
278+ // Reset backoff only after a fully authenticated connection so that repeated
279+ // auth failures (e.g. token_missing) still produce growing retry delays (#94).
280+ _ws.resetReconnectAttempt ();
259281 _updateState (_state.copyWith (
260282 status: NodeStatus .paired,
261283 connectedAt: DateTime .now (),
0 commit comments