Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
| Windows node connection generation, cancellation, start ordering, recovery, events, and telemetry | `NodeConnectionCoordinator` | authoritative |
| Bootstrap/shared/device credential handoff, durable clear gate, and operator token recovery timing | `BootstrapTokenLifecycle` | authoritative |
| Device role-upgrade approval, confirmation, and bounded node reconnect queue | `DevicePairApprovalCoordinator` | authoritative |
| Gateway wire protocol range, minimal `hello-ok` validation, and sanitized compatibility state | `GatewayProtocolContract` + `GatewayProtocolCompatibility` | authoritative |
| Capability UI metadata | `NodeCapabilityUiCatalog` (planned) | planned |
| Capability registration/gating | `NodeCapabilityRegistrationPolicy` (planned) | planned |
| Local MCP exposure policy | `McpCapabilityPolicy` (planned) | planned |
Expand Down Expand Up @@ -195,6 +196,8 @@ leading and trailing pipe. Columns, in order:
| connect-envelope | authoritative | src/OpenClaw.Shared/OpenClawGatewayClient.cs + src/OpenClaw.Shared/WindowsNodeClient.cs | connect envelope wire shape, auth field mapping, and v3/v2 signing arguments | ConnectEnvelopeBuilder | callers explicitly select role, scopes, credential profile, and lifecycle/fallback state | the builder cannot infer credential precedence; exact v3/v2 signing bytes and protocol 3/4 remain unchanged | ConnectEnvelopeBuilderTests.Build_CompleteProfileMatrix_PreservesWireShapeAndSigningArguments | golden | - |
| gateway-connect-inline-closed | closed | src/OpenClaw.Shared/OpenClawGatewayClient.cs + src/OpenClaw.Shared/WindowsNodeClient.cs | anonymous connect envelope, auth dictionary, and direct signature/payload construction | ConnectEnvelopeBuilder | explicit role/scope/credential profile selection, lifecycle/fallback state and persistence, redacted logging, transport send | both clients delegate connect construction without moving credential precedence into the builder | GatewayProtocolCoreClosureTests.GatewayClients_DoNotReintroduce_InlineConnectEnvelopeConstruction | source-shape | when both clients are removed or no longer initiate gateway connect handshakes |
| gateway-pending-inline-closed | closed | src/OpenClaw.Shared/OpenClawGatewayClient.cs | pending maps and locks plus Track-Take-Clear and chat-send helper families | PendingRequestRegistry | request-id creation, timeout policy, response parsing/routing, transport | the client delegates registration, take, removal, and disconnect drain so request ids cannot leak and only one completion wins | GatewayProtocolCoreClosureTests.OpenClawGatewayClient_DoesNotReintroduce_InlinePendingRequestTracking | source-shape | when OpenClawGatewayClient is removed or no longer issues correlated requests |
| gateway-protocol-contract | authoritative | ConnectEnvelopeBuilder + src/OpenClaw.Shared/OpenClawGatewayClient.cs + src/OpenClaw.Shared/WindowsNodeClient.cs | duplicated wire-version range, ad hoc successful hello acceptance, and raw mismatch interpretation | GatewayProtocolContract + GatewayProtocolCompatibility | the builder retains exact envelope construction; clients retain credential precedence, signature fallback, generation fencing, and role-specific success side effects | Windows advertises Gateway protocols 3 through 4 and validates the minimal hello-ok shape before success side effects; after the Gateway accepts that range, an integer hello-ok protocol at or above 3 is accepted because it reports the Gateway current constant rather than a negotiated selection; structured mismatch details remain finite and sanitized relative to the advertised range | GatewayProtocolContractTests.SupportedRange_IsThreeThroughFour | behavioral | - |
| gateway-protocol-literals-closed | closed | ConnectEnvelopeBuilder | private minProtocol and maxProtocol numeric literals | GatewayProtocolContract | the builder serializes its existing envelopes using the shared constants | operator and node connect envelopes advertise the same 3-through-4 protocol range | GatewayProtocolContractTests.Clients_use_shared_contract_for_protocol_range | source-shape | when connect envelopes no longer carry a Gateway protocol range |
| ui-dispatcher | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | UI-thread marshaling abstraction for presentation code | IUiDispatcher | App and existing WinUI code may call DispatcherQueue directly until the view-model migration | presentation view models depend on IUiDispatcher not a concrete DispatcherQueue | UiDispatcherContractTests.PageViewModel_ReceivesRegisteredDispatcher | behavioral | - |
| navigation-scope | authoritative | src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs | page view-model activation/deactivation and disposal lifetime | NavigationScopeManager | HubWindow keeps frame navigation back-stack and rail selection | transient page view models are activated on navigation and deactivated then disposed on navigate-away | NavigationScopeManagerTests.NavigatingAway_DeactivatesAndDisposesPreviousViewModel | behavioral | - |
| composition-root | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | presentation-layer service construction and wiring | AppServiceRegistration | App remains the composition root and owns non-DI service lifetimes | one validated root ServiceProvider; App-owned singletons registered as instances are never disposed by the container | AppServiceRegistrationTests.Dispose_DoesNotDisposeAppOwnedInstanceSingletons | behavioral | - |
Expand Down
7 changes: 5 additions & 2 deletions docs/CONNECTION_PROTOCOL_RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ stable release-validation evidence.
The managed gateway release pin is not the WebSocket protocol pin. Windows
currently advertises `minProtocol: 3` and `maxProtocol: 4`; the gateway reports
its current protocol constant in `hello-ok.protocol`, not a per-connection
negotiated value. Windows records that value for diagnostics but does not
currently branch behavior on it. Current upstream gateways use the protocol-3
negotiated value. After the gateway accepts the advertised range, Windows records
that integer constant for diagnostics and requires it to be at least
`MinimumSupportedVersion`; a lower or malformed value is a terminal protocol
failure. A structured `PROTOCOL_MISMATCH` rejection determines which side falls
outside the advertised range. Current upstream gateways use the protocol-3
N-1 node window only when both `role` and `client.mode` are `node` and the
client range does not support the gateway's current protocol. Because Windows
advertises `maxProtocol: 4`, its node connection uses protocol 4 against a
Expand Down
19 changes: 17 additions & 2 deletions docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,25 @@ The tray exports gateway lifecycle diagnostics when an endpoint is configured:
- structured state logs in the `OpenClaw.Telemetry.Connection` category

Lifecycle attributes are limited to role, operation, outcome, coarse error
category, and finite operator/node/overall states. Gateway URLs, IDs, device
IDs, pairing request IDs, credentials, error messages, and diagnostic-ring
category, finite operator/node/overall states, and these protocol fields:

- `openclaw.protocol.client`: the current client wire protocol integer; Windows
currently advertises support from protocol 3 through protocol 4
- `openclaw.protocol.gateway`: the finite relative bucket `older`, `current`,
`newer`, or `unknown`, relative to the advertised 3-through-4 range; this
bucket is not itself a compatibility verdict
- `openclaw.protocol.compatibility`: `unknown`, `compatible`,
`gateway_too_old`, `gateway_too_new`, or `mismatch`

Gateway package versions are not protocol values and are not exported by this
instrumentation. Gateway URLs, IDs, device IDs, pairing request IDs,
credentials, integrity values, error messages, raw frames, and diagnostic-ring
text are not exported.

Only an explicit protocol mismatch maps to the `protocolmismatch` error
category. Unknown or otherwise unclassified Gateway errors map to
`internalerror`; they are never promoted to protocol mismatch.

The operator phase spans distinguish local credential/client/tunnel preparation,
WebSocket transport establishment, and the gateway challenge/hello handshake.
The Windows node initiates its gateway connection: its prepare span includes
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions src/OpenClaw.Connection/ConnectionStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ internal sealed class ConnectionStateMachine
private string? _operatorError;
private OpenClaw.Shared.GatewayErrorKind? _operatorErrorKind;
private string? _nodeError;
private OpenClaw.Shared.GatewayErrorKind? _nodeErrorKind;
private OpenClaw.Shared.GatewayProtocolCompatibility _operatorProtocolCompatibility =
OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
private OpenClaw.Shared.GatewayProtocolCompatibility _nodeProtocolCompatibility =
OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
private string? _operatorCredentialSource;
private string? _nodeCredentialSource;
private GatewayCredentialResolutionStatus? _operatorCredentialStatus;
Expand Down Expand Up @@ -135,6 +140,8 @@ public void SetNodeEnabled(bool enabled)
{
_nodeState = RoleConnectionState.Disabled;
_nodeError = null;
_nodeErrorKind = null;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
_nodeCredentialSource = null;
_nodeCredentialStatus = null;
_nodeCredentialFallbackUsed = false;
Expand All @@ -145,6 +152,8 @@ public void SetNodeEnabled(bool enabled)
{
_nodeState = RoleConnectionState.Idle;
_nodeError = null;
_nodeErrorKind = null;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
}
RebuildSnapshot();
}
Expand All @@ -157,6 +166,9 @@ public void Reset()
_operatorError = null;
_operatorErrorKind = null;
_nodeError = null;
_nodeErrorKind = null;
_operatorProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
_operatorCredentialSource = null;
_nodeCredentialSource = null;
_operatorCredentialStatus = null;
Expand All @@ -177,6 +189,8 @@ public void StartNodeConnecting()
{
_nodeState = RoleConnectionState.Connecting;
_nodeError = null;
_nodeErrorKind = null;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
RebuildSnapshot();
}
}
Expand Down Expand Up @@ -215,6 +229,26 @@ internal void SetOperatorErrorKind(OpenClaw.Shared.GatewayErrorKind? kind)
RebuildSnapshot();
}

internal void SetOperatorProtocolCompatibility(
OpenClaw.Shared.GatewayProtocolCompatibility compatibility)
{
_operatorProtocolCompatibility = compatibility;
RebuildSnapshot();
}

internal void SetNodeErrorKind(OpenClaw.Shared.GatewayErrorKind? kind)
{
_nodeErrorKind = kind;
RebuildSnapshot();
}

internal void SetNodeProtocolCompatibility(
OpenClaw.Shared.GatewayProtocolCompatibility compatibility)
{
_nodeProtocolCompatibility = compatibility;
RebuildSnapshot();
}

/// <summary>Update node info (device ID, pairing status, optional request ID) in the snapshot.</summary>
internal void SetNodeInfo(
string? deviceId,
Expand Down Expand Up @@ -302,6 +336,7 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)
_operatorState = RoleConnectionState.Connecting;
_operatorError = null;
_operatorErrorKind = null;
_operatorProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
break;

case ConnectionTrigger.ConnectRequestSent:
Expand Down Expand Up @@ -369,6 +404,9 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)
_operatorError = null;
_operatorErrorKind = null;
_nodeError = null;
_nodeErrorKind = null;
_operatorProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
_operatorCredentialSource = null;
_nodeCredentialSource = null;
_operatorCredentialStatus = null;
Expand All @@ -385,6 +423,7 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)
_operatorState = RoleConnectionState.Connecting;
_operatorError = null;
_operatorErrorKind = null;
_operatorProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
break;

case ConnectionTrigger.ReconnectSuppressed:
Expand All @@ -401,21 +440,29 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)
case ConnectionTrigger.NodeConnected:
_nodeState = RoleConnectionState.Connected;
_nodeError = null;
_nodeErrorKind = null;
break;

case ConnectionTrigger.NodeDisconnected:
if (_nodeErrorKind == OpenClaw.Shared.GatewayErrorKind.ProtocolMismatch)
break;

_nodeState = RoleConnectionState.Idle;
_nodeError = null;
_nodeErrorKind = null;
_nodeProtocolCompatibility = OpenClaw.Shared.GatewayProtocolCompatibility.Unknown;
break;

case ConnectionTrigger.NodePairingRequired:
_nodeState = RoleConnectionState.PairingRequired;
_nodeError = null;
_nodeErrorKind = null;
break;

case ConnectionTrigger.NodePaired:
_nodeState = RoleConnectionState.Connected;
_nodeError = null;
_nodeErrorKind = null;
break;

case ConnectionTrigger.NodePairingRejected:
Expand All @@ -437,6 +484,11 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)

private void RebuildSnapshot()
{
var (protocolCompatibility, protocolRole) =
GatewayConnectionSnapshot.DeriveProtocolCompatibility(
_operatorProtocolCompatibility,
_nodeProtocolCompatibility,
_nodeEnabled);
Current = Current with
{
OverallState = GatewayConnectionSnapshot.DeriveOverall(_operatorState, _nodeState, _nodeEnabled),
Expand All @@ -448,18 +500,23 @@ private void RebuildSnapshot()
OperatorCredentialFallbackUsed = _operatorCredentialFallbackUsed,
OperatorCredentialBootstrapRequired = _operatorCredentialBootstrapRequired,
OperatorCredentialDetail = _operatorCredentialDetail,
OperatorProtocolCompatibility = _operatorProtocolCompatibility,
OperatorPairingRequired = _operatorState == RoleConnectionState.PairingRequired,
// Clear requestId when no longer in PairingRequired to prevent stale reads
OperatorPairingRequestId = _operatorState == RoleConnectionState.PairingRequired
? Current.OperatorPairingRequestId : null,
NodeConnectionIntended = _nodeEnabled,
NodeState = _nodeState,
NodeError = _nodeError,
NodeErrorKind = _nodeErrorKind,
NodeCredentialSource = _nodeCredentialSource,
NodeCredentialStatus = _nodeCredentialStatus,
NodeCredentialFallbackUsed = _nodeCredentialFallbackUsed,
NodeCredentialBootstrapRequired = _nodeCredentialBootstrapRequired,
NodeCredentialDetail = _nodeCredentialDetail,
NodeProtocolCompatibility = _nodeProtocolCompatibility,
ProtocolCompatibility = protocolCompatibility,
ProtocolCompatibilityRole = protocolRole,
// Clear requestId when no longer in PairingRequired to prevent stale reads
NodePairingRequestId = _nodeState == RoleConnectionState.PairingRequired
? Current.NodePairingRequestId : null,
Expand Down
Loading