# Managed Wrapper Findings Source assembly: `current\aahClientManaged.dll` Tooling used: `ilspycmd 10.0.0.8330` ## Confirmed Shape `aahClientManaged.dll` is a mixed-mode C++/CLI wrapper. The public managed API is under the `ArchestrA` namespace, but query execution calls native `HistorianClient` methods through C++ pointers rather than plain P/Invoke methods. `dumpbin /dependents` confirms the deployed wrapper does not import `aahClient.dll` or `aahClientCommon.dll`. It is a self-contained mixed-mode wrapper over system DLLs and the CLR (`mscoree.dll`), so the active wrapper path must be captured inside `aahClientManaged.dll` or at system/WCF boundaries. The production read surface maps to these wrapper types: - `ArchestrA.HistorianAccess` - `ArchestrA.HistorianConnectionArgs` - `ArchestrA.HistoryQueryArgs` - `ArchestrA.HistoryQuery` - `ArchestrA.HistoryQueryResult` - `ArchestrA.EventQueryArgs` - `ArchestrA.EventQuery` - `ArchestrA.HistorianEvent` ## Connection Facts `HistorianConnectionArgs` defaults: - `TcpPort = 32568` - `ServerName = "localhost"` - `ReadOnly = true` - `PacketTimeout = 1000` - `ConnectionType = Process` - `Compression = false` - `IntegratedSecurity = false` `HistorianConnectionType` is a flags enum: - `Process = 1` - `Event = 2` The wrapper keeps separate client slots for process/history and event connections. `HistorianAccess.OpenConnection` can return `true` while the native connection is still pending. Callers must poll `HistorianAccess.GetConnectionStatus` and wait for `ConnectedToServer = true` and `Pending = false` before starting queries. Starting `HistoryQuery.StartQuery` immediately after `OpenConnection` can return native `notConnected` (`242`). ## History Query Contract `HistoryQuery.StartQuery` eventually calls native `HistorianClient.StartDataQuery`. It does not build or call `RetrievalServiceContract.StartQuery2` directly from managed C#. The managed wrapper converts `HistoryQueryArgs` into native strings, arrays, selector buffers, and enum values, then hands those to the native `HistorianClient` implementation. That native boundary is the next capture point for exact data-query request bytes. Confirmed arguments include: - Retrieval mode: cast from `ArchestrA.HistorianRetrievalMode`. - Query format: `0`. - Summary type: `0`. - Tag count and UTF-16 tag pointer array. - Start/end timestamps converted with `DateTime.ToUniversalTime().ToFileTime()`. - Resolution, value deadband, time deadband. - Time zone string: `UTC`. - Version type from `DataVersion`. - Interpolation, timestamp, quality, value selector, and aggregation enums. - Option and filter strings converted as UTF-16 buffers. - Selected query-column buffer. - Max states. - Output query handle. Decompiled selected-column stream construction is: - `ushort` value `1` - 8 bytes of `QueryColumnSelector` state after `QueryColumnSelector.SelectNonSummaryColumns` Focused IL extraction of `DataQueryRequest.Save>` confirmed several request-buffer details used by the .NET 10 serializer probe: - request packet version is `ushort 3` only when option text is exactly `NoOption`; otherwise it is `ushort 9` and the option string is serialized. - offset `300` is serialized as a packed `CQTIFlags` `ushort` before option/filter text, not the query-column selector flags. The low byte is interpolation (`None` is stored as `255`; `SystemDefault` `254` is normalized to `255`), bits `8..11` are `HistorianTimestampRule`, and bits `12..15` are `HistorianQualityRule`. Ordinary full reads therefore write `0x01FF` (`Interpolation=None`, `TimestampRule=End`, `QualityRule=Extended`). - the query-column selector is serialized later as `ushort 1` followed by the exact 8-byte selector state. - after the MDS and storage redundant endpoint blocks, the request writes the raw 8-byte resolution tick field again. - empty `CMetadataNamespace.Save` writes flag byte `1` and three empty `SaveScrambled` strings; each empty scrambled string is `ushort 1` followed by a single zero compressed-string marker byte. - default `AutoSummaryParameters.Save` writes `ushort 2`, two zero `int64` values, five empty compressed strings, and the trailing zero GUID. - `CValueSelector.Save`, `CStateCalcSelector.Save`, and `CQTIFlags.Save` each write a single `ushort` value. - `SRedundantEndpoint.Save` writes endpoint name and then endpoint entries as node-name and pipe-name strings; there is no numeric port field in this block. The resulting byte count and pointer are passed to `StartDataQuery` as the selected query-column buffer arguments. The decompiled call site passes the following high-level arguments to `HistorianClient.StartDataQuery`: - client pointer from the process connection slot - retrieval mode as `INSQL_QUERYTYPE` - query format `0` - summary type `0` - tag count and native UTF-16 tag array - source count `0` and null source array - start/end as UTC FILETIME values - resolution, value deadband, and time deadband - literal timezone `UTC` - data version, interpolation type, timestamp rule, quality rule - option text, with `NoOption` substituted when the option is not available - value selector, aggregation type, selected-column byte count and buffer - filter text and max states - output query handle and native error structure This explains the current managed WCF negative evidence: the reconstructed `DataQueryRequest.Save` buffer is only one downstream native serialization artifact, while the public wrapper path reaches that serializer through `HistorianClient.StartDataQuery`. `BaseQueryArgs.ProcessQueryArgs` clears `Resolution` to `0` for `Full`, `Delta`, and `Slope` retrieval modes. `HistoryQuery.MoveNext` calls native `HistorianClient.GetNextRow`. ## Runtime Method Pointer Evidence `tools\AVEVA.Historian.NativeTraceHarness` can now dump prepared CLR runtime method pointers with `--dump-method-pointers `. This is reverse- engineering-only evidence for hook targeting; it is not production SDK code. Important artifacts: - `runtime-method-pointers-startquery-latest.json` - `runtime-method-pointers-startdataquery-latest.json` - `runtime-method-pointers-getnextrow-latest.json` - `runtime-method-pointers-starteventquery-latest.json` The dump must include `assembly.ManifestModule.GetMethods(...)` because many useful mixed-mode functions are global `` methods rather than ordinary managed type methods. Confirmed runtime-discoverable methods include: - `.Query.StartDataQuery` - `.HistorianClient.StartDataQuery` - `.ClientApp.StartDataQuery` - `.CRetrievalConnectionWCF.StartQuery2` - `.Query.GetNextRow` - `.HistorianClient.GetNextRow` - `.Query.StartEventQuery` - `.HistorianClient.StartEventQuery` The prepared function pointers are process-specific CLR/JIT entry addresses. They are outside the loaded `aahClientManaged.dll` image in the current runs (`FunctionPointerInModule = false`, `FunctionPointerRva = null`), so they must not be treated as stable DLL RVAs. They can still be useful if discovered inside the same target process immediately before attaching a hook, or if a CLR profiler/rewrite pass resolves the runtime target directly. A same-process Frida pass using these absolute runtime pointers is also negative evidence so far. `Attach-NativeTraceHarnessRuntimePointerCapture.ps1` launched the native trace harness, wrote the runtime pointer snapshot before `HistoryQuery.StartQuery`, paused, generated a temporary Frida script from those exact addresses, and installed 37 hooks. The native direct history read still succeeded, but no hook `enter`/`leave` callbacks fired. This suggests `MethodHandle.GetFunctionPointer()` is not the callable edge used by the mixed-mode wrapper path in a way Frida can intercept directly. ## IL Instrumentation Evidence Focused `ildasm` excerpts are stored as: - `ildasm-historyquery-startquery-excerpt-latest.il` - `ildasm-historyquery-movenext-excerpt-latest.il` - `ildasm-classlist-filtered-latest.txt` These excerpts confirm `ArchestrA.HistoryQuery.StartQuery` is IL with a normal metadata-token call to the mixed-mode target: - call site: `IL_02d2` - target: `HistorianClient.StartDataQuery` - metadata token: `060055E4` `ArchestrA.HistoryQuery.MoveNext` has the same shape for row retrieval: - call site: `IL_0054` - target: `HistorianClient.GetNextRow` - metadata token: `0600588D` This is now a proven instrumentation path. A dnlib `NativeWrite` copy of `aahClientManaged.dll` loads and runs in the native trace harness when placed in a disposable copied DLL folder. A reverse-only IL rewrite of `.Query.StartDataQuery` token `0600574B` inserted a logger immediately after `DataQueryRequest.Save>` and `endstream`. The instrumented wrapper completed a local direct read and emitted the serialized `StartDataQuery.Request` buffer before the native WCF call. The read-boundary instrumentation now also logs the explicit `HistorianClient.GetNextRow` `arg.1` query handle before dumping the `arg.2` row memory. A combined local read captured these records in one session: - `StartDataQuery.Request`: length `251`, SHA-256 `543ea11af87607044067a0274b1da423cef2acbb7b4f4fab137af023a7153d7f` - `GetNextRow.QueryHandle`: value `1` - `GetNextRow.DataQueryResultRow`: length `512`, SHA-256 `702f5248cf8319e3e02da33678ed97dfaa43666bddb88c42101d5290990a4198` The harness snapshot from the same run shows `HistoryQuery.queryHandle = 1`, so the managed wrapper field, native `GetNextRow` argument, and row buffer are now correlated without relying on inferred state. The next successful run instrumented the native WCF retrieval boundary itself. `CRetrievalConnectionWCF.StartQuery2` sent the same 251-byte request through `RetrievalServiceContract.IRetrievalServiceContract2.StartQuery2` with `clientHandle = 256524568` and `queryRequestType = 1`. The server returned a 31-byte response buffer, SHA-256 `4c062b5ce8181308f0f46bfd8c6088acb52e6ade94401651b7d3ccc8952edfb5`, and WCF query handle `2967`. `CRetrievalConnectionWCF.GetNextQueryResultBuffer2` then used `clientHandle = 256524568` and `queryHandle = 2967` and returned a 570-byte result buffer, SHA-256 `f4126e0610a4c63b3dff0e41e8d15e51d75fdbc736f3ec490e7c66d1bb31638d`. This separates the two handles: `HistoryQuery.queryHandle = 1` is the client-side `HistorianClient` query table handle, while `/Retr` uses the server query handle returned from `StartQuery2`. A later correlation run added scalar logging inside `.HistorianClient.OpenConnection` and `.Query.StartDataQuery`. `OpenConnection` wrote legacy native handle `2`, and `Query.StartDataQuery` read the same `*(HistorianClient + 8)` value immediately before the vtable call into the common retrieval layer. The WCF transport method still called `IRetrievalServiceContract2.StartQuery2` with a different transient retrieval client handle. That means the managed read blocker is not the public `HistoryQuery.queryHandle`, not the legacy native handle, and not the `DataQueryRequest` bytes; it is the session mapping maintained by the native common retrieval layer before the WCF contract call. The sanitized correlation summary is stored in `query-handle-correlation-latest.json`. `aahClientCommon.CServerClient.GetHandle` returns `*(this + 2528)`, but an instrumented copy of that accessor emitted no records during the same successful history read path. A direct metadata-token reference scan also found no IL call sites for token `060017F9`. The handle translation is therefore either direct field access or a virtual/calli continuation below `CRetrieval.StartQuery2`, not this accessor. Additional probes ruled out that assumed `CRetrieval` branch for the successful local WCF read path: instrumented copies of `aahClientCommon.CRetrieval.StartQuery2`, `aahClientCommon.CSrvRetrievalConnection.StartQuery`, and `CRetrievalConsoleClient.StartQuery` emitted no records while the WCF boundary hooks still fired. The active handoff is `aahClientCommon.CClientCommon.StartQuery` token `06002E86`. `CClientCommon.StartQuery` performs the missing handle lookup immediately before calling the WCF retrieval method. The value returned by the `CClient` vtable call at IL offset `0x01A3` exactly matches the `CRetrievalConnectionWCF.StartQuery2` client handle. After the WCF call returns, the `queryHandle` pointer contains the same server query handle later passed to `GetNextQueryResultBuffer2`. This narrows the remaining session problem to reproducing or replacing the native `CClient` open/session state that produces the `/Retr` client handle, not rebuilding the data-query request envelope. `CClientBase.OpenConnection` uses the same vtable offset `24` as a handle accessor. At the beginning of open it returns `0`; after the secondary open branch at IL offset `0x06D4` succeeds, the accessor returns the same transient handle consumed by `CClientCommon.StartQuery` and WCF `StartQuery2`. The primary open branch did not fire in the local integrated read capture. The next concrete reverse-engineering target is the secondary open vtable call and the WCF/native contract behind it. That secondary branch is `CHistoryConnectionWCF.OpenConnection3`, which wraps `IHistoryServiceContract2.OpenConnection2`. The successful local read capture logged a 1346-byte OpenConnection3 request, a 42-byte response, and an empty error buffer. In that response, byte `0` is `0x03` and the transient `/Retr` client handle is UInt32 little-endian at offset `1`. Deserializing the 42-byte response initializes the vtable offset `24` handle. Raw request bytes include local identity/process metadata, so only hashes and redacted handle relationships are documented in `openconnection3-correlation-latest.json`. The first captured full-history request for deterministic tag `OtOpcUaParityTest_001.Counter` is 251 bytes with SHA-256 `3581ef3b42b59b46503d1aa0127fa60fe4b40943e419aeab99e47e4683888851`. The sanitized byte dump is stored in `startdataquery-request-buffer-latest.json`; raw generated binaries and runtime capture files stay under ignored `artifacts\reverse-engineering`. This fixture corrected the managed serializer in three places: - `BatchSize` is not serialized in `DataQueryRequest`. - A reserved `uint32 0` follows `SkipRows`. - `AutoSummaryParameters` serializes as version `1`, two zero `int64` values, five zero flag bytes, and a final `uint32 0`. The production-side WCF request serializer now has a byte-for-byte test against this native buffer. The same instrumentation captured a `TimeWeightedAverage` aggregate `StartDataQuery.Request` for a one-minute resolution window. It is also 251 bytes; SHA-256 is `954874bf851bdea6333b8a8159f036e19b124b7a5febefb0cb9c9a8564b20981`, and the sanitized dump is stored in `startdataquery-timeweightedaverage-request-buffer-latest.json`. Confirmed aggregate differences versus full-history: - request query type at offset `0x02` is `5` - first resolution field at offset `0x1E` is `double 600000000.0` - later resolution field at offset `0xD0` is `int64 6000000000000` - `BatchSize = 3` from `HistoryQueryArgs` is still not serialized in the `DataQueryRequest` buffer The managed serializer now has a second byte-for-byte fixture test for this aggregate request. An `Interpolated` request with the same one-minute resolution also serializes to 251 bytes. SHA-256 is `fc3a2fcc28d1926d2bd1de477e306cb0930e80a3327be6309b6e834e2951ca26`, and the sanitized dump is stored in `startdataquery-interpolated-request-buffer-latest.json`. Confirmed interpolated request details: - request query type at offset `0x02` is `3` - first resolution field at offset `0x1E` is `double 600000000.0` - later resolution field at offset `0xD0` is `int64 6000000000000` - the rest of the default filter, metadata namespace, endpoint, and auto-summary tail match the full-history and aggregate fixtures The managed serializer now has byte-for-byte fixture coverage for full history, time-weighted aggregate, and interpolated data requests. A second IL rewrite instrumented `.HistorianClient.GetNextRow` token `0600588D` immediately after the call to `Query.GetNextQueryResultRow` token `060058AF`. The successful one-row local read emitted a 512-byte snapshot of the native `DataQueryResultRow*` output structure. The sanitized first-pass dump is stored in `getnextrow-dataqueryresultrow-memory-latest.json`; the artifact hash is `2c2cb06988187c1bd7793a52a71f33599542a69d5e83885c583de8bf3df5d43b`. Confirmed basic row-memory offsets for this full-history sample: - offset `0x00`: `uint32` tag key `238` - offset `0x28`: FILETIME UTC start timestamp `2026-05-01T14:26:51.1956318Z` - offset `0x30`: `uint32` quality `133` - offset `0x34`: `uint32` quality detail `248` - offset `0x38`: `uint32` OPC quality `192` - offset `0x80`: `double` percent good `100` The managed `HistoryQueryResult` reflection snapshot from the same run maps these fields to tag name `OtOpcUaParityTest_001.Counter`, value `0`, quality `133`, OPC quality `192`, quality detail `248`, and percent good `100`. For `TimeWeightedAverage`, `GetNextRow` emits a different row-memory layout for time bounds. The sanitized summary is in `getnextrow-timeweightedaverage-memory-latest.json`. Confirmed aggregate row offsets from the two returned rows: - offset `0x00`: `uint32` tag key `238` - offset `0x28`: managed `EndDateTime` - offset `0x150`: managed `StartDateTime` - offset `0x30`: `uint32` quality - offset `0x34`: `uint32` quality detail - offset `0x38`: `uint32` OPC quality - offset `0x80`: `double` percent good The third logged row snapshot duplicated the second row after the managed enumerator reached native result code `30` (`No more data`), so row-result instrumentation should correlate captures with the managed `MoveNext` return status before treating every logged struct as a logical row. `Interpolated` row memory follows the same time-bound layout in the captured fixture. The sanitized summary is in `getnextrow-interpolated-memory-latest.json`. - offset `0x00`: `uint32` tag key `238` - offset `0x28`: managed `EndDateTime` - offset `0x150`: managed `StartDateTime` - offset `0x30`: `uint32` quality `0` - offset `0x34`: `uint32` quality detail `248` - offset `0x38`: `uint32` OPC quality `192` - offset `0x80`: `double` percent good `100` The second logged interpolated row snapshot duplicated the first after `No more data`, matching the aggregate instrumentation behavior. Integrated-auth native harness evidence (`tools\AVEVA.Historian.NativeTraceHarness`, latest JSON artifact `docs\reverse-engineering\native-trace-harness-integrated-read-latest.json`) confirms these runtime state transitions for `OtOpcUaParityTest_001.Counter`: - `HistorianAccess.OpenConnection` succeeds and sets `clientHandlerArray[0] = 1` for the process connection slot. - `HistoryQuery` starts with `queryHandle = 0`, `retrievalMode = Cyclic`, `dataVersion = Original`, and no result object. - `HistoryQueryArgs` for the read uses `RetrievalMode = Full`, `DataVersion = Latest`, `BatchSize = 1`, `QualityRule = Extended`, `TimeStampRule = End`, `InterpolationType = None`, `Filter = NoFilter`, `ValueSelector = Auto`, and `AggregationType = Total`. - `HistoryQuery.StartQuery` succeeds, normalizes `dataSourceId` from `null` to empty string, allocates a `HistoryQueryResult`, and assigns `queryHandle = 1`. - First `MoveNext` returns one row with tag key `238`, value `0`, quality `133`, OPC quality `192`, quality detail `248`, and percent good `100`. Later local runs show a distinction between local direct history reads and the WCF history path: - `--direct-connection` succeeds for Galaxy-discovered historized tags such as `TestMachine_001.TestHistoryValue`, returning one row with value `0`, quality `133`, OPC quality `192`, and quality detail `248`. - non-direct local history reads currently open and connect successfully but throw `SEHException` inside native `DataQueryResponse.Load` during `HistoryQuery.StartQuery`; the hardened harness now reports this as `StartQueryException` instead of crashing. - event queries still succeed over the local non-direct path, so the Historian service is reachable and the failure is specific to data query response handling on the local WCF history path. The harness accepts `--retrieval-mode`, `--start-utc`, `--end-utc`, and `--resolution-ticks` so scenarios can be repeated with fixed UTC windows. Latest native wrapper evidence artifacts: - `native-trace-harness-full-latest.json`: `Full`, one raw row. - `native-trace-harness-cyclic-latest.json`: `Cyclic`, 60-second resolution, two rows. - `native-trace-harness-interpolated-latest.json`: `Interpolated`, 60-second resolution, one row. - `native-trace-harness-timeweightedaverage-latest.json`: `TimeWeightedAverage`, 60-second resolution, two rows. `HistoryQueryResult` allocates a native `DataQueryResultRow` of 544 bytes. Confirmed offsets used by the managed wrapper: - `0`: tag key (`uint`) - `4`: data type discriminator (`int`) - `8`: tag name (`std::wstring`) - `40`: end timestamp (`FILETIME`) - `48`: quality (`ushort` read through `int`) - `52`: quality detail (`uint`) - `56`: OPC quality (`ushort` read through `int`) - `72`: string value (`std::wstring`) - `104`: numeric value (`double`) - `128`: percent good (`double`) - `336`: start timestamp (`FILETIME`) - `400`: interpolation/timestamp/quality packed flags (`ushort`) - `416`: source tag (`std::wstring`) - `448`: source server (`std::wstring`) - `488`: actual resolution in FILETIME ticks ## Enum Values Mirrored Into The .NET 10 Scaffold `HistorianRetrievalMode`: - `Cyclic = 0` - `Delta = 1` - `Full = 2` - `Interpolated = 3` - `BestFit = 4` - `TimeWeightedAverage = 5` - `MinimumWithTime = 6` - `MaximumWithTime = 7` - `Integral = 8` - `Slope = 9` - `Counter = 10` - `ValueState = 11` - `RoundTrip = 12` - `StartBound = 13` - `EndBound = 14` Other confirmed enums: - `HistorianInterpolationType`: `StairStep = 0`, `Linear = 1`, `SystemDefault = 254`, `None = 255` - `HistorianTimestampRule`: `Start = 0`, `End = 1`, `None = 2` - `HistorianQualityRule`: `Extended = 0`, `Good = 1`, `None = 2`, `Optimistic = 3` - `HistorianValueSelector`: `Auto = 1`, `First = 2`, `Last = 3`, `Integral = 4`, `StdDev = 5`, `Minimum = 6`, `Maximum = 7`, `Average = 8` - `HistorianAggregationType`: `Minimum = 0`, `Maximum = 1`, `Average = 2`, `Total = 3`, `Percent = 4`, `MinContained = 5`, `MaxContained = 6`, `TotalContained = 7`, `AverageContained = 8`, `PercentContained = 9` ## Retrieval Metadata Contract Decompiled WCF retrieval contract signatures in `aahClientManaged.dll` match the .NET 10 scaffold for the scalar and byte-buffer metadata calls: - `GetTagTypeFromName(uint clientHandle, string tagName, out uint tagType)` - `IsOriginalAllowed(uint clientHandle, out bool isAllowed)` - `IsManualTag(uint clientHandle, string tagName, out bool isManual)` - `GetTagInfoFromName(uint clientHandle, string tagName, out uint tagMetadataByteCnt, out byte[] tagMetadata)` - `GetTagInfosFromId(uint handle, uint tagIdsSize, byte[] tagIds, ref uint sequence, out uint tagInfosSize, out byte[] tagInfos)` - `GetTagInfosFromName(uint handle, uint tagNamesSize, byte[] tagNames, ref uint sequence, out uint tagInfosSize, out byte[] tagInfos)` Remote managed evidence against `10.100.0.48:32568` confirms the integrated session handle works for scalar retrieval calls. For `OtOpcUaParityTest_001.Counter`, `GetTagTypeFromName` returns type `1`, `IsManualTag` returns `false`, and `GetTagInfoFromName` returns `238` with no metadata bytes. `GetTgByNm` and `GetTg` buffer probes currently return the same `238` and no output bytes for all tried encodings. That keeps exact metadata buffer construction unresolved. `ArchestrA.TagQuery.StartQuery` is a separate tag browse/query path. It calls `HistorianClient.StartTagQuery` with a UTF-16 tag-filter pointer, an output query handle, an output tag count, and `SError*`. The WCF continuation in `CRetrievalConnectionWCF.StartTagQuery` serializes a request buffer before calling `IRetrievalServiceContract3.StartTagQuery(handle, byte[], out byte[], out byte[])`. IL instrumentation of `CRetrievalConnectionWCF.StartTagQuery` token `06004A15` captured the exact WCF request buffer for deterministic OData filter `TagName eq 'OtOpcUaParityTest_001.Counter'`. The request is 92 bytes with SHA-256 `af1dbcdd3eb0ad91a18882c22252aa74aff82998e96a39b63415ab4792a962ac`; sanitized field notes are stored in `starttagquery-request-buffer-latest.json`. Confirmed tag-query request fields: - offset `0x00`: `ushort` marker `26449` (`0x6751`) - offset `0x02`: `ushort` version `1` - offset `0x04`: `uint32` filter character length - offset `0x08`: UTF-16LE filter text without a trailing null The same instrumentation also captures the successful WCF response byte array. For the current local run the response is 8 bytes with SHA-256 `db49223a2cf9616171322e5325816a7a579582ebdce91c2f89df8df7aa8aac01`. Confirmed start-tag-query response fields: - offset `0x00`: `uint32` query handle - offset `0x04`: `uint32` tag count The local native tag query succeeds with that OData filter and returns `TagCount = 1`, tag name `OtOpcUaParityTest_001.Counter`, and metadata summary `TagKey = 238`, `TagDataType = 4`, `TagStorageType = 3`, `EngineeringUnit = None`. Plain tag name filters reach the server but fail with native failure code `1`, while wildcard filters fail as invalid OData/WWFilter syntax. Response buffers and the native `CTagMetadata` vector layout for `GetTagInfo` still need byte-level instrumentation. `TagQuery.GetTagNames` calls `HistorianClient.GetTagNames`, while `TagQuery.GetTagInfo` calls `HistorianClient.GetTagInfos` and then `HistorianTag.LoadFromTagMetadata` for each 224-byte `CTagMetadata` entry in the returned native vector. The separate wildcard-style browse path is not `TagQuery`; method inventory shows it as `HistorianClient.StartLikeTagNameSearch` / `GetLikeTagnames` with WCF continuations `CRetrievalConnectionWCF.StartLikeTagNameSearch` / `GetLikeTagnames`. IL instrumentation of `aahClientCommon.CClientCommon.GetTagInfos` token `06002EC9` captured the pre-deserialization response stream used by `TagQuery.GetTagInfo`. For the deterministic tag `OtOpcUaParityTest_001.Counter`, the stream is 106 bytes with SHA-256 `77b2bf720d8888f08a1499a8162e706c2cef567a1f6d74d7e92efe0cd3e3e34b`; sanitized details are stored in `tagquery-gettaginfo-response-latest.json`. Confirmed `GetTagInfo` response stream fields for the current fixture: - offset `0x0000`: `uint32` tag count - offset `0x0004`: 4-byte native data-type descriptor; `CTagUtil.GetDataValueType` maps this fixture to public `HistorianDataType = 4` - offset `0x0008`: type GUID - offset `0x0018`: `uint32` tag key - offset `0x001C`: compact ASCII tag name: marker `0x09`, `uint16` byte length, then bytes - offset `0x003C`: compact ASCII metadata provider, observed as `MDAS` - offset `0x0043`: native tag class byte - offset `0x0044`: storage type byte - offset `0x0045`: deadband type byte - offset `0x0046`: interpolation type byte The same instrumentation captured the native `std::vector` after `Load>`; the vector has 224-byte elements. `HistorianTag.LoadFromTagMetadata` reads tag key from vector offset `0x0030`, tag name from `0x0038`, description from `0x0058`, storage type from `0x00C1`, deadband type from `0x00C2`, and inactive status from vector offset `0x0034`. The wildcard browse path is confirmed separately through managed WCF calls to `Retr.StartLikeTagNameSearch` and `Retr.GetLikeTagnames`. With integrated Windows `Open2` and filter `OtOpcUaParityTest%`, `StartLikeTagNameSearch` returns `0`, and `GetLikeTagnames` returns one 66-byte buffer with SHA-256 `2d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1`. The buffer layout for the one-tag fixture is `uint32 count`, then for each tag `uint32 UTF-16 character length` and UTF-16LE tag name bytes. Sanitized details are stored in `like-tag-browse-response-latest.json`. ## Event Query Contract `EventQuery.StartQuery` calls native `HistorianClient.StartEventQuery`. Confirmed arguments include: - Start/end timestamps converted to FILETIME. - Event count. - Skip count. - Event order as `ushort`. - Filter block pointer. - Time zone string: `UTC`. - Output query handle. The decompiled wrapper passes event query type value `1` into `HistorianClient.StartEventQuery` for ordinary event reads. This is distinct from the WCF retrieval `queryRequestType`: `Query.StartEventQuery` stores `ushort 3` immediately before calling `CRetrievalConnectionWCF.StartEventQuery`, so the WCF operation must use `queryRequestType = 3`. `EventQueryRequest.Save>` now has a byte-level field order from `ildasm`: - constant request version `ushort 5` - start and end FILETIME values from the embedded `TIMERANGE` - `eventCount` as `uint32` - `skipCount` as `uint32` - `eventOrder` as `uint16` - `queryType` as `uint16` - `EventQueryFilters.Save` - result buffer size as `uint32`; native construction passes `0x10000` - timezone `std::wstring`; ordinary reads pass `UTC` - `CMetadataNamespace.Save` - `EventQueryFilters.SaveSelect` `EventQueryFilters.Save` writes `ushort 0`, a `uint32` filter count, each filter, a continuation flag byte, and only writes continuation FILETIME/GUID when that flag is non-zero. Empty event reads therefore serialize the filter block as `00 00 00 00 00 00 00`. `SaveSelect` writes a `uint32` selected property count followed by compressed strings; default event reads write count zero. `EventQuery` allocates `EventQueryFilters` as a 72-byte native object. It rejects filters on `EventTime`, limits filter count to 500, and converts filter property names and values to UTF-16 strings. `EventQuery.MoveNext` calls `HistorianClient.GetNextRow` and then `HistorianEvent.CreateHistorianEvent`. Integrated-auth native harness event evidence (`native-trace-harness-event-latest.json`) confirms: - Opening with `HistorianConnectionType = Event` succeeds and reaches `ConnectedToServer = true`. - `HistorianAccessUtil.SetConnectionMode(Event, integratedSecurity: true)` produces Open2 connection mode `0x501` (`1281`). The process read-only integrated mode is `0x402` (`1026`). - Native `HistorianAccess.OpenConnection` creates the event connection at `ConnectionIndex.Event` and then calls `CreateDefaultEventTag`. `CreateDefaultEventTag` builds a synthetic `HistorianTag` named `CM_EVENT`, sets description `AnE Event`, engineering unit `NONE`, data type `10`, and calls `AddTagInternal`; the native access snapshot then has `clientHandlerArray = [0, 1]` and `eventTagHandle = 10000000`. - `EventQueryArgs` uses UTC `StartDateTime` / `EndDateTime`, `QueryType = Events`, `EventOrder = Ascending`, and `EventCount` from `--max-rows`. - `EventQuery.StartQuery` succeeds with native `Success`. - A seven-day local-dev window returned three sanitized event rows; raw event IDs and opaque event source details are not written by the harness. - A managed WCF `StartEventQuery` probe using this reconstructed version-5 empty-filter request and `queryRequestType = 3` reaches `/Retr.StartEventQuery` but returns `false` with no native error buffer. This remains true for process-mode Open2 (`1026`) and event-mode Open2 (`1281`), and for the returned Open2 handle, the native `ClientApp` event slot handle `1`, and the observed event tag handle `10000000`. - `CRetrievalConnectionWCF.StartEventQuery` has a server-interface-version branch: interface versions `<= 2` call `/Retr.StartQuery2`; newer versions call `/Retr.StartEventQuery`. The managed probe now tries both operations with the same reconstructed request. On the local 2020 server, both operation paths still return `false` with no native error buffer, so the mismatch is likely connection/session state rather than this dispatch branch. - Because native event open performs `CreateDefaultEventTag` before event query start, the next gap is likely the `HistorianClient.AddHistorianTag` / `AddTagInternal` setup for `CM_EVENT`, not the basic event request field order or WCF contract signature. - Follow-up decompile of `aahClientCommon.CClient.RegisterTag` shows this part is client-local: it calls `RegisterRealtimeTag`, inserts the tag id into local sets, marks the cached `CTagInfo` as registered, and flips synchronization flags. There is still a server-visible registration path elsewhere (`IHistoryServiceContract.RTag` / storage `RTag`), but `CreateDefaultEventTag` does not directly prove its byte payload. - The server-visible `RTag2` WCF edge is now partially mapped. Decompiled `CHistoryConnectionWCF.RegisterTags` converts a native `_GUID Handle` to a string, calls `IHistoryServiceContract2.RegisterTags2` (`RTag2`), and passes a byte buffer serialized as `uint32 count` followed by `count` 16-byte `_GUID` values. The local managed probe `wcf-register-event-tag` tried empty, zero-GUID, and event-handle-shaped GUID vectors with Open2-derived handle candidates. All failed before event query start could succeed: first-16 handle candidates returned native error `51`, while the bytes 4-19 .NET GUID candidate reached the server but returned native error `1`. This makes simple `RTag2` replay unlikely to be the missing event setup by itself; the richer `AddHistorianTag`/`CTagMetadata` path remains the lead. - `HistorianClient.ConvertEventTagToTagMetadata` now has a concrete event-tag metadata map from IL/native data: - synthetic tag name: `CM_EVENT` - description from `CreateDefaultEventTag`: `AnE Event` - event tag id: `353b8145-5df0-4d46-a253-871aef49b321` - common ArchestrA event type id: `5f59ae42-3bb6-4760-91a5-ab0be01f2f27` - ArchestrA event type id: `f3ef1a17-fd27-4c3c-84cf-bac55c0e47de` - converted `CDataType` byte: `5` - storage type passed to `CTagMetadata.Initialize`: `2` - `CTagMetadata.Save>` writes a one-byte serialization version, a two-byte optional-field mask, the `CDataType` byte, the 16-byte tag id, and compressed strings for present string fields. Short ASCII UTF-16 strings use the compact form `09 00 `, so `CM_EVENT` serializes as `09 08 00 43 4d 5f 45 56 45 4e 54`. - A direct managed replay of this approximate event metadata through `IHistoryServiceContract.AddTags` (`AddT`) is negative evidence: `wcf-add-event-tag` opened `/Hist-Integrated`, tried both a raw `CTagMetadata` payload and a vector-count-prefixed payload, and both returned `AddReturnCode = 4` with no output buffer. The subsequent `StartEventQuery` still returned `false`. This means the missing native event setup is not reproduced by the current `AddT` approximation. - A fake WCF capture server on an alternate port exposed `/Hist`, `/Hist-Integrated`, and `/Retr`, but the native event harness still reached the real local Historian and the fake server saw no calls. The `HistorianConnectionArgs.TcpPort` field does not redirect this native WCF path sufficiently for server-side capture. - The expanded Frida event add-tag pass hooked the candidate MethodDef RVAs before `OpenConnection` and the native event query still succeeded, but no enter/leave callbacks fired. MethodDef RVAs remain insufficient hook targets for this mixed-mode wrapper; use CLR method rewriting/profiling or lower-level transport/API capture next. - IL instrumentation of `.Query.StartEventQuery` token `0600574A` now captures the exact native `EventQueryRequest` buffer immediately after `EventQueryRequest.Save>` and `endstream`. A local seven-day event query succeeded and returned three rows while logging a 65-byte request. SHA-256 is `6b955b02087047a3199a8c74f3eee85c3b49aaa29b05de12eff2dd536f2da0d5`, and the sanitized dump is stored in `starteventquery-request-buffer-latest.json`. The managed event query serializer now has a byte-for-byte test against this native fixture. - A matching IL rewrite of `.HistorianClient.GetNextRow` token `06005965` captures native `EventQueryResultRow*` memory immediately after `Query.GetNextEventQueryResultRow` returns. The successful local seven-day event query emitted three 2048-byte raw snapshots under ignored `artifacts\reverse-engineering`; the sanitized summary is stored in `getnexteventrow-memory-latest.json`. Confirmed first-pass event row evidence: - offset `0x18`: managed `HistorianEvent.EventTime` as FILETIME UTC for all three rows - `ReceivedTime` was not present as a direct FILETIME value in the captured 2048-byte native struct snapshot - managed event type was `User.Write.Secured` for all three rows, but the raw struct contains pointer-like fields and no safely decoded inline event-type string yet This keeps event result parsing unresolved, but it proves the IL rewrite path can capture both event request bytes and native event row memory without adding production dependencies. ## Tag Query Session Evidence `CRetrievalConnectionWCF.StartTagQuery` does not carry the tag filter in the latest successful WCF call. Combined IL instrumentation of native `OpenConnection` plus tag-query start captured a successful local tag metadata query where the WCF `StartTagQuery` request was only: - marker/version bytes: `51 67 01 00` - SHA-256: `17956e4fbe53d5edc0f9170203b013432e4afcc0591c795a10522a98d9fce926` - response bytes: `0C 00 00 00 01 00 00 00` - parsed response: query handle `12`, tag count `1` The tag filter `TagName eq 'OtOpcUaParityTest_001.Counter'` was already processed before this WCF request. The native WCF handle string is an uppercase GUID generated for the native retrieval session; it is not the numeric Open2 handle, the Open2 storage session id, the first 16 Open2 bytes, or the full Open2 output. Managed replay of both the older 92-byte filter-shaped candidate and the latest 4-byte header-only request with all Open2-derived handle candidates returned native errors `51` or `1`. Conclusion: public metadata should not be wired through `StartTagQuery`. `BrowseTagNamesAsync` can stay on the proven `StartLikeTagNameSearch` / `GetLikeTagnames` path. `GetTagMetadataAsync` now uses the direct `GetTagInfoFromName` WCF path, which returned a 98-byte metadata record for the deterministic parity tag. More guessed `StartTagQuery` envelopes are not the useful next target; the missing evidence is the native session registration or the earlier metadata-layer call that binds the filter to the retrieval GUID. ## Immediate Unknowns These still require native ABI inspection and packet captures: - Session handshake frame. - Authentication frame. - Query frame envelope and length/correlation fields. - Native `StartDataQuery` message type identifier. - Native `StartEventQuery` message type identifier. - `GetNextRow` request/response frame shape. - Error frame layout. See `native-exports.md` for the first PE export-table pass over `aahClient.dll`.