How GatewayRegistry Manages Multiple Gateway Records and Active Gateway Switching in openclaw-windows-node

GatewayRegistry is a thread-safe, pure-data catalog that stores all gateway endpoints in a locked list, tracks the current active gateway via a single ID string, and raises a Changed event whenever the registry mutates.

In the openclaw/openclaw-windows-node repository, GatewayRegistry serves as the central authority that manages multiple gateway records and handles active gateway switching for the Windows node client. Located in src/OpenClaw.Connection/GatewayRegistry.cs, this class never holds runtime connection state; it only maintains and retrieves gateway information while ensuring atomic updates and disk persistence. Every public operation is synchronized so that background refreshes and UI actions cannot corrupt the gateway list or active selection.

Core Architecture of GatewayRegistry

The registry is built around two private fields guarded by a single _lock object. This design keeps the component thread-safe without exposing synchronization details to callers.

Thread-Safe Record Storage

All known gateways are stored in a private List<GatewayRecord> _records defined at line 16 of src/OpenClaw.Connection/GatewayRegistry.cs. Any read or write to this list occurs inside a lock (_lock) block, making every query and mutation atomic. The registry is intentionally a pure-data component; it does not manage sockets, streams, or protocol state.

Active Gateway Tracking

The registry stores the currently selected gateway in a private string field named _activeId. Callers retrieve the active record through GetActive(), which looks up _activeId under lock and returns the matching GatewayRecord (lines 99–102). An ActiveGatewayId property exposes the raw ID for callers that only need the identifier. This separation allows the active gateway to be changed instantly in memory before any disk I/O or reconnection occurs.

Managing Multiple Gateway Records

Because users can pair several devices, the registry provides three mutation methods that all respect the same lock and eventing contract.

Adding and Updating Records with AddOrUpdate

The AddOrUpdate(record) method (lines 71–85) inserts a new GatewayRecord or replaces an existing entry when the incoming record has the same Id. After the lock is released, the method raises the Changed event, delivering a shallow copy of the full record list to any subscriber. This guarantees that UI components and background services always receive a consistent snapshot.

var registry = new GatewayRegistry();
registry.AddOrUpdate(new GatewayRecord
{
    Id = "node-01",
    Url = "https://node01.openclaw.local",
    IsLocal = true,
    AuthToken = legacyToken
});

Atomic In-Place Mutation

When a caller needs to transform an existing record—such as clearing an expired token or updating a timestamp—the Update(id, updater) method (lines 110–124) runs the supplied delegate inside the lock. Because the updater executes while the registry is locked, concurrent modifications cannot overwrite each other, preventing lost updates.

registry.Update("node-01", record =>
{
    record.AuthToken = null;
    record.LastSeen = DateTimeOffset.UtcNow;
});

Removing Gateways

Remove(id) (lines 87–96) deletes the specified record and automatically clears _activeId if the removed gateway was the active one. This prevents the registry from returning stale active references after a gateway is unpaired.

Active Gateway Switching

Switching the active gateway is an explicit, two-step process that separates in-memory selection from disk persistence and reconnection.

Selecting the Active Gateway

SetActive(id) writes the new _activeId under lock (lines 99–102). No network or disk I/O happens inside this call; it simply updates the pointer. This design keeps the UI responsive and lets the application decide when to persist or reconnect.

End-to-End Switching Flow

A typical user-driven switch—such as choosing a different gateway from the tray menu—follows this sequence:

  1. The menu handler calls registry.SetActive(selectedId).
  2. The UI layer or connection manager observes the registry.Changed event and calls registry.GetActive() to fetch the fresh GatewayRecord.
  3. The connection manager reconnects using the credentials stored in that record, while the UI updates the displayed gateway name.

Because every step that touches _records or _activeId is protected by _lock, a background refresh cannot race with the user’s click. The connection manager in src/OpenClaw.Tray.WinUI/Services/ConnectionManagerWindowsNodeConnector.cs consumes the registry exactly this way, resolving credentials from the active record after each switch.

Persisting Gateway Data to Disk

The registry serializes its entire state through Save() and Load() (lines 129–166). Save() writes a RegistryData object—containing both the full record list and the active ID—to gateways.json using an atomic temp-file rename so that a crash during write never leaves a corrupt file. Load() deserializes the JSON back into _records and _activeId, restoring the last known state on application startup.

registry.SetActive("node-02");
registry.Save(); // Atomic write to gateways.json

Notifying UI Components via Events

Every mutation method—AddOrUpdate, Remove, and Update—raises the Changed event (lines 25–26). Subscribers such as the tray menu, setup wizard, and connection manager receive a shallow copy of the record list and can re-render or reconnect accordingly. This event-driven model decouples the registry from the presentation layer while ensuring every component stays synchronized with the active gateway.

Migrating Legacy Settings

The registry also handles first-run upgrades through MigrateFromSettings. According to the implementation in GatewayRegistry.cs, this method copies legacy token fields into a fresh GatewayRecord, marks the new record as active, and moves the old identity file into a per-gateway directory. The migration path is exercised in tests/OpenClaw.Connection.Tests/GatewayRegistryMigrationTests.cs, validating that legacy data transitions cleanly into the new registry format.

Summary

  • GatewayRegistry keeps all gateway records in a List<GatewayRecord> protected by a private _lock, making every read and write atomic.
  • The active gateway is tracked by a single _activeId string; callers switch it via SetActive(id) and read it via GetActive().
  • Mutations such as AddOrUpdate, Update, and Remove persist thread-safe changes and raise the Changed event with a snapshot of all records.
  • Disk serialization uses an atomic temp-file rename in Save(), storing both the record list and active ID to gateways.json.
  • UI and connection components in ConnectionManagerWindowsNodeConnector.cs and SetupWizardRunner.cs rely on the Changed event to refresh views and reconnect after an active gateway switch.

Frequently Asked Questions

How does GatewayRegistry prevent duplicate gateway entries?

The AddOrUpdate method checks for an existing record with the same Id. If a match is found, the old record is replaced rather than appended, ensuring the internal list never contains duplicate identities for a single gateway.

What happens to the active gateway when a record is removed?

Remove(id) deletes the record and automatically clears _activeId if it referenced the removed gateway. This prevents the registry from pointing to a non-existent endpoint after unpairing.

Is GatewayRegistry thread-safe for concurrent UI and background operations?

Yes. Every public method acquires the private _lock before touching _records or _activeId. According to the openclaw-windows-node source code, this synchronization guarantees that concurrent tray-menu clicks and background refreshes cannot corrupt the list or active selection.

How is the registry state preserved across application restarts?

Save() serializes a RegistryData object containing the full record list and active ID to gateways.json using an atomic temp-file rename. On startup, Load() deserializes this file back into memory, restoring the exact state from the previous session.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →