# Implementation Status ## Completed - Production SDK targets `net10.0` and has no AVEVA binary references. - Public API includes the full intended parity surface: - TCP probe - raw, aggregate, at-time, and block history reads - event reads - tag browse and metadata calls - connection, store-forward, and system-parameter status calls - **`EnsureTagAsync`** for analog Float/Double/Int2/Int4/UInt4 with optional `ApplyScaling=true` for distinct MinRaw/MaxRaw persistence (live-verified end-to-end against `localhost`; SQL post-check confirms `AnalogTag.Scaling=1` and distinct raw bounds when the flag is set) - **`DeleteTagAsync`** (live-verified) - **AddS2 (write samples) is architecturally blocked** — server runtime cache only ingests from configured IOServers / Application Server pipelines, not from `HistorianAccess.AddTag`-only flows. Three independent reproduction attempts confirmed the same `129 "Tag not found in cache"` failure even with the real wwTagKey, fresh sessions, and 8s settle waits. Not a protocol gap. - Internal protocol scaffolding exists: - `HistorianConnection` - `HistorianFrameReader` - `HistorianFrameWriter` - `Historian2020ProtocolDialect` - Evidence-backed WCF scaffolding exists: - WCF service names for `Hist`, `Retr`, `Storage`, `Stat`, and `Trx` - contract interfaces for history, history extensions, retrieval, retrieval extensions, and status - `application/x-mdas` message encoder wrapper - Net.TCP binding and endpoint factory for port `32568` - bounded `wcf-start-query` harness options (`--max-attempts` and `--timeout-seconds`) so negative query probes do not have to run the full matrix - live local and remote `GetV` evidence for `/Hist`, `/Retr`, `/Stat`, and `/Trx` - live `GetV` evidence for `/HistCert` using MDAS over WCF Net.TCP transport security - `ValidateClientCredential` token wrapping and the native NTLM negotiate `VERSION` flag adjustment as isolated, tested protocol primitives - `ProbeAsync` now uses fully managed WCF/MDAS `GetV` calls instead of a raw TCP socket check. - Managed `Hist.OpenConnection` reaches server logic, but the older scalar operation expects the native password/session packet. - Managed `Hist.Open2` now reaches server logic with a version-1 byte buffer. Empty credentials return custom native error `171` (`AuthenticationFailed`), confirming the basic byte-buffer framing and UTF-16 string encoding. - Managed integrated Windows `Open2` succeeds when the same version-1 buffer is sent to `/Hist-Integrated` with WCF Windows transport security. `/Hist` with that binding fails before the operation call because the upgrade is not supported on the plain history endpoint. - The same managed integrated Windows `Open2` flow succeeds remotely against `:32568/Hist-Integrated`; the returned handle is accepted by `Retr.IsOriginalAllowed`. The stored artifact redacts the session output buffer and transient handle value. - The native `aahClientManaged.dll` path is confirmed to open successfully with integrated Windows auth after polling `GetConnectionStatus` until pending clears. - The native integrated read path is confirmed to reach query execution: a deliberately missing tag returns native `TagNotFound` (`127`) instead of connection/authentication failure. - `scripts\Find-GalaxyHistorizedTags.ps1` queries the local Galaxy Repository (`localhost`, database `ZB`, Windows auth) for non-array dynamic attributes with `HistoryExtension`. - `OtOpcUaParityTest_001.Counter` is confirmed as a live native read fixture candidate. A 1440-minute native wrapper read returned one row with timestamp `2026-04-30T11:00:29.4340342Z`, `Value = 0`, `Quality = 133`, `OpcQuality = 192`, and `QualityDetail = 248`. - A Frida attach pass can see `aahClientManaged.dll` and install hooks at candidate mixed-mode RVAs, but those `base + RVA` hooks do not fire during the successful read. The next capture approach must intercept CLR/WCF managed byte-array calls rather than raw method RVAs. See `frida-aahclientmanaged-hook-pass.md`. - `tools\AVEVA.Historian.NativeTraceHarness` is a reverse-engineering-only .NET Framework harness for native integrated reads. It records sanitized reflection snapshots around `OpenConnection`, `StartQuery`, and `MoveNext`. The latest run confirms `StartQuery` assigned `queryHandle = 1` and returned tag key `238`, value `0`, quality `133`, OPC quality `192`, and quality detail `248` for `OtOpcUaParityTest_001.Counter`. It now supports fixed UTC windows plus configurable retrieval mode and resolution; latest artifacts cover `Full`, `Cyclic`, `Interpolated`, and `TimeWeightedAverage`. - The native trace harness also supports `--scenario event` with an event connection. The latest event artifact confirms `EventQuery.StartQuery` succeeds and returns three sanitized local-dev event rows for a seven-day window. - `scripts\Attach-NativeTraceHarnessWinsockCapture.ps1` and `scripts\frida\aahclientmanaged-winsock.js` can attach before native `OpenConnection` and hook Winsock plus common file/pipe APIs. Localhost, `127.0.0.1`, and the machine LAN IP all completed successful native reads without observed `connect`/`send`/`recv`, `CreateFile`/`ReadFile`/`WriteFile`, or `NtCreateFile`/`NtReadFile`/`NtWriteFile` payload events. This is negative evidence that the installed local Historian path uses an in-process/local optimization rather than the remote Net.TCP transport path. - An expanded Frida method pass added decompiled `QueryColumnSelector` and `HistorianClient.StartDataQuery`/`GetNextRow` MethodDef RVAs. Hooks installed where Frida allowed them, but no enter/leave events fired during successful native reads. MethodDef RVAs from the mixed-mode assembly are not the actual CLR/native dispatch targets. - The native trace harness can now dump prepared CLR runtime method pointers for mixed-mode `` methods such as `HistorianClient.StartDataQuery`, `CRetrievalConnectionWCF.StartQuery2`, `HistorianClient.GetNextRow`, and `HistorianClient.StartEventQuery`. The corrected artifacts show these pointers are process-specific CLR/JIT entry addresses outside the loaded `aahClientManaged.dll` image, not stable DLL RVAs. - `scripts\Attach-NativeTraceHarnessRuntimePointerCapture.ps1` automates a same-process Frida pass against those runtime pointers. It successfully generated a pre-`StartQuery` pointer snapshot and installed 37 absolute hooks while the native direct history read was paused. The read then succeeded, but no hook `enter`/`leave` callbacks fired. This is negative evidence for using `MethodHandle.GetFunctionPointer()` addresses as direct Frida hook targets. - `scripts\Attach-NativeTraceHarnessAahClientExportCapture.ps1` and `scripts\frida\aahclient-exports.js` attempted to hook the procedural `mdas_*` exports from `aahClient.dll`. A successful local direct history read did not load `aahClient.dll`, and `dumpbin /dependents` confirms `aahClientManaged.dll` does not import `aahClient.dll` or `aahClientCommon.dll`. A separate `-DumpLoadedModules` run showed only `aahClientManaged.dll` among the current AVEVA DLLs. The exported `mdas_*` ABI is likely a separate native client surface, not the active C++/CLI wrapper boundary used by this harness. - `scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1` and `scripts\frida\aahclientmanaged-system-boundary.js` now hook file I/O, `Nt*File`, `NtDeviceIoControlFile`, DNS, exported Winsock calls, `WSAIoctl`, `mswsock` extension exports, Secur32, Crypt32, and NetAPI. Local direct and same-machine remote-IP reads produce no boundary callbacks beyond hook installation. A Debian relay run confirms the relay TCP connection is owned by the harness PID, but the same exported user-mode hooks still see no connect/send/recv/device-control callbacks before the security reset. - `scripts\Run-PktmonDebianRelayCapture.ps1` records pktmon metadata for the Debian relay path without retaining raw payload bytes. The latest history run captured TCP packet metadata for `:32568`, converted the ETL to a text report, deleted the ETL, and wrote a summary confirming `PayloadBytesCaptured = false` and `RawEtlDeleted = true`. - Focused `ildasm` excerpts now confirm `ArchestrA.HistoryQuery.StartQuery` has an IL call at `IL_02d2` to `HistorianClient.StartDataQuery` token `060055E4`, and `HistoryQuery.MoveNext` has an IL call at `IL_0054` to `HistorianClient.GetNextRow` token `0600588D`. IL-rewrite instrumentation is now validated: a dnlib-written wrapper copy can run in a disposable DLL folder, and an instrumented `.Query.StartDataQuery` captured a 251-byte native `DataQueryRequest` buffer during a successful local direct read. - The captured full-history buffer corrected the managed `HistorianDataQueryProtocol` serializer. It now matches the native wrapper byte-for-byte for the deterministic `OtOpcUaParityTest_001.Counter` fixture. The buffer hash is `3581ef3b42b59b46503d1aa0127fa60fe4b40943e419aeab99e47e4683888851`. - The same instrumentation path now captures the native `DataQueryResultRow*` memory after `GetNextRow`. The first full-history row confirms tag key, timestamp, quality, OPC quality, quality detail, and percent-good offsets; the artifact hash is `2c2cb06988187c1bd7793a52a71f33599542a69d5e83885c583de8bf3df5d43b`. - The read-boundary instrumentation now logs `GetNextRow` `arg.1` as an explicit `queryHandle` scalar before dumping `arg.2` row memory. A combined local read captured `StartDataQuery.Request` SHA `543ea11af87607044067a0274b1da423cef2acbb7b4f4fab137af023a7153d7f`, `GetNextRow.QueryHandle = 1`, and `GetNextRow.DataQueryResultRow` SHA `702f5248cf8319e3e02da33678ed97dfaa43666bddb88c42101d5290990a4198` in the same session. - A follow-up combined IL instrumentation pass correlated the native open, query, WCF retrieval, and row-read handles. `HistorianClient.OpenConnection` wrote legacy handle `2`, and `Query.StartDataQuery` read the same value as its `ClientHandleCandidate` immediately before entering the common retrieval layer. The successful `CRetrievalConnectionWCF.StartQuery2` call then used a different transient WCF retrieval client handle. This proves the missing read parity state is the native client/session mapping below the legacy `ClientApp` handle, not the 251-byte `DataQueryRequest` payload. - Instrumenting `aahClientCommon.CServerClient.GetHandle` token `060017F9` produced no records during a successful local history read, even while the surrounding open/query/WCF hooks fired. Direct metadata-token scanning also found no IL references to that accessor. The transient WCF retrieval handle is not obtained through `GetHandle` on this path. - Instrumenting `aahClientCommon.CRetrieval.StartQuery2`, `aahClientCommon.CSrvRetrievalConnection.StartQuery`, and `CRetrievalConsoleClient.StartQuery` also produced no records in the successful WCF read path. The active path is instead `aahClientCommon.CClientCommon.StartQuery` token `06002E86`. - The `CClientCommon.StartQuery` instrumentation captured the missing handle boundary: legacy handle `2` enters `Query.StartDataQuery`, then `CClientCommon.StartQuery` calls a `CClient` vtable function at IL offset `0x01A3`. The returned transient value exactly equals the client handle used by `CRetrievalConnectionWCF.StartQuery2` and `GetNextQueryResultBuffer2`. The WCF server query handle returned by `StartQuery2` is copied back through the `queryHandle` pointer, while the public managed `HistoryQuery.queryHandle` remains the wrapper value `1`. Sanitized evidence is in `cclientcommon-startquery-correlation-latest.json`. - `CClientBase.OpenConnection` confirms that same vtable offset `24` is the handle accessor: the initial value is `0`, then the secondary open branch succeeds and the post-open value exactly matches the later `CClientCommon.StartQuery` and WCF `StartQuery2` client handle. The primary open branch did not emit records on this local integrated path. Sanitized evidence is in `cclientbase-open-correlation-latest.json`; the next target is the secondary open vtable call at IL offset `0x06D4`. - The secondary open branch resolves to `CHistoryConnectionWCF.OpenConnection3` token `06004059`. It calls `IHistoryServiceContract2.OpenConnection2` with a 1346-byte request and gets a 42-byte response with empty error. The deserialized response initializes the vtable offset `24` handle later used by `CClientCommon.StartQuery` and `/Retr.StartQuery2`. In the captured 42-byte response, byte `0` is `0x03` and the transient `/Retr` client handle is UInt32 little-endian at offset `1`. `CClientInfo.DeserializeOpenConnectionOutParams` token `06004008` confirms the response layout: byte protocol version, `uint32` client handle, 16 session bytes, one FILETIME, and for version `3` an additional FILETIME. The observed five-byte tail is preserved as opaque trailing data until a caller or native field assignment is identified. `CClientInfo.SerializeOpenConnectionInParams4` token `06004003` confirms the observed request starts with protocol byte `6`, a 16-byte client key, and a boolean content selector. The observed selector is `0`, so the active content branch is `SerializeOpenConnectionInParams2Content` token `06004005`, whose field order matches the existing native version-3 content serializer: host string, UInt16 secret length plus secret bytes, client type, connection mode, metadata namespace, two strings, and client common info. `CMetadataNamespace.Save` uses compact/scrambled empty strings in this envelope, so the empty metadata namespace is 10 bytes rather than the older 13-byte plain layout. With that correction, managed replay with the observed 1026-byte zero credential block reaches server credential validation instead of packet-version rejection. Both `/Hist-Integrated` Windows transport and `/HistCert` certificate transport now return native error `171` with an extended message saying the server context could not be found. That makes the leading 16-byte OpenConnection3 key the next evidence target. A same-run memory-correlation pass now proves request bytes `1..16` exactly match `CClientInfo +1240`, and request byte `17` matches `CClientInfo +1608`. `CClientInfo.SetContextKey` token `0600386E` copies its GUID argument to the same `CClientInfo +1240` field, but no direct IL caller exists. The actual source is now narrowed to `CClientBase.ConfigureOpenConnection` token `0600388C`: it calls native `CClientContext.AuthenticateClient` token `06005DCB` on `CClientBase +2112`, then copies 16 bytes from `CClientBase +2176` (`CClientContext +64`, the `GetContextKey` location) to `CClientBase +1480`. Since this aligns with `CClientInfo +1240`, the prefix is the context key that native `AuthenticateClient` validates before OpenConnection3, not an arbitrary fresh managed replay GUID. Runtime instrumentation around `AuthenticateClient` confirms the field equals the generated client key before authentication, changes during the native auth path, then matches the copied `CClientInfo` field and the OpenConnection3 request prefix. A direct managed `Hist-Integrated.ValidateClient2` probe reaches the service but fails with native error type `4`/code `51` before `ExchangeKey`. Sanitized IL instrumentation of `CHistoryConnectionWCF.ValidateClient` (`06004044`) and `CHistoryConnectionWCF.GetClientKey` (`06004041`) was present in an isolated wrapper copy and the logger smoke test passed, but a successful native integrated history read emitted no `ValidateClient2` or `ExchangeKey` records. That negative result rules out the obvious managed WCF auth methods for this path. Native disassembly of `CClientContext.AuthenticateClient` (`06005DCB`, RVA `0x298BA0`) shows it uses Secur32 `AcquireCredentialsHandleW` with package `Negotiate`, creates a context GUID at `CClientContext +64`, and loops through `CHistoryConnectionWCF.ValidateClientCredential` / WCF `ValCl`. Instrumenting `ValCl` confirms two successful native rounds: a 69-byte client input to 239-byte server output, then a 93-byte client input to a one-byte terminal output. The client input envelope is one round byte, a 4-byte little-endian token length, then an NTLMSSP token; the first native input prefix is `01400000004e544c4d5353500001000000b7b218e209000900370000000f000f`. System-boundary Frida evidence now confirms that these wrapped payloads come directly from SSPI: native calls `AcquireCredentialsHandleW` with package `Negotiate`, then two `InitializeSecurityContextW` calls for `NT SERVICE\aahClientAccessPoint`. The raw SSPI output token lengths are 64 and 88 bytes, matching the 69- and 93-byte `ValCl` bodies after the 5-byte AVEVA wrapper is added. The first call returns `SEC_I_CONTINUE_NEEDED` (`0x90312`) and the second returns success. Native disassembly of `CClientContext.AuthenticateClient` now maps the loop directly: it is native-only, calls `UuidCreate`, stores the context GUID at `CClientContext +64`, calls `AcquireCredentialsHandleW`, and enters a loop at VA `0x180298DE0`. The internal helper at VA `0x180298F30` calls `InitializeSecurityContextW`, uses request flags `0x2081C` on the first round and `0x81C` on later rounds, then writes the `ValCl` stream envelope as round byte, UInt32 token length, and token bytes. The outer loop copies `CClientContext +64` as the context key and calls the connection's `ValidateClientCredential` virtual method; in the captured local read, `CClientContext +0x50` is zero, so the normal connection-object virtual path at VA `0x180298E95` is selected. A stable IL-side memory window around `CClientContext.AuthenticateClient` shows the embedded `CClientContext` mutates only four regions in the first 256 bytes during successful auth: pointer-sized fields at offsets `+8`, `+16`, and `+24`, plus the context key at `+64..+79`. Dereferencing the `+8` target shows SSPI/security-package metadata strings such as `Schannel`, `Microsoft Kerberos V1.0`, `TSSSP`, `System.Core`, and `Default TLS SSP`. The `+16` target is partially decoded as pointer-rich native state: nested targets at offsets `0` and `8` have the same pointer/count shape and no readable ASCII or UTF-16 Historian payload strings, while the nested target at offset `64` is all zero. The `+24` value is not safe to treat as a directly readable buffer. This narrows the missing managed replay state to native `CClientContext` object state and server acceptance of that client context key, rather than the SSPI token bytes alone. A managed `NegotiateAuthentication` probe can reproduce that first wrapped input exactly after setting the NTLM `VERSION` negotiate flag, but standalone `ValCl` still fails with native error type `4`/code `1` on both `/HistCert` and `/Hist-Integrated`. That means the active blocker is not the first token envelope; it is native connection/session prerequisite state around proxy initialization or server-side context registration. A follow-up instrumented local read shows the native path constructs `CHistoryConnectionWCF`, then calls `GetInterfaceVersion` before auth. The first `GetInterfaceVersion` `InitializeProxy` path succeeds and `SetManagedPtr` sets the ready flag to `1`; by both successful `ValCl` rounds, `COperation.Start2` succeeds with error type/code `0`, the existing proxy is not faulted, and the reconnect flag is `0`. Branch instrumentation now shows the local native path uses connection mode `1`, enters `CWcfConfig.ConfigurePipeProxy`, builds the uncompressed local `/Hist` named-pipe endpoint shape, and does not use `ConfigureTcpProxy`. Static IL inspection of `ConfigurePipeProxy` confirms this path builds a `NetNamedPipeBinding` with `Security.Mode = None`, `TransactionFlow = false`, native-sized `MaxBufferSize`/`ReaderQuotas.MaxArrayLength`, and the `aahMDASEncoder.ClientBinding` wrapper. It then creates the channel through the static `ChannelFactory.CreateChannel(binding, endpoint)` helper and sets `IContextChannel.OperationTimeout` from the native timeout-minutes argument. Managed pipe probes using the same static-factory channel shape, with and without eager channel open, still fail in the same way. A managed named-pipe `ValCl` replay reaches `GetInterfaceVersion` version `11` and reproduces the first wrapped token hash, but is still rejected at round `0` with native error type `4`/code `1`; explicitly running the managed calls under the current Windows token does not change that result. Native handle summaries show uppercase GUID text, and managed lowercase-handle replay still fails, so handle casing is not the mismatch. Skipping `GetInterfaceVersion` and avoiding explicit WCF channel open in the managed TCP probe also returns native error type `4`/code `1`, so the mismatch is not just transport or token bytes. Because native `GetInterfaceVersion` creates its proxy while impersonating the stored Windows identity, the managed pipe probe was extended to create/open the channel inside the same current-token impersonation scope; that still fails at `ValCl` round `0`, with and without also wrapping the operation calls. A local System Platform log check shows the managed pipe `ValCl` failures correspond to `aahClientAccessPoint` warnings: `ValidateClientCredential caught exception: System.NullReferenceException` at `HistoryService.ValidateClientCredential` line `1593`. Enabling named-pipe transport security is rejected as a binding mismatch, which confirms the native uncompressed pipe binding shape. A new direct .NET Framework WCF probe using the same `aa`/`Hist` contract, MDAS content type, uncompressed named pipe, and SSPI-generated first token also fails with native error type `4`/code `1` and the same server log exception. That rules out a simple .NET 10 WCF regression: the success condition belongs to AVEVA's mixed/native wrapper state, not a plain full-framework WCF client. Static IL inspection of `COperation.Start2` shows it is only a local gate: it checks operation-priority and bandwidth controls and can set local gate-failure error codes `243` or `150`, but it does not call a WCF service operation. Static inspection of the local `aahClientAccessPoint.exe` service now maps the receiving side of this failure. `HistoryService.ValidateClientCredential` parses the WCF `handle` as a GUID, allocates a `CServerBuffer`, copies the `ValCl` byte array into that buffer, and calls native `CServerNode.ProcessServerToken`. That native method parses exactly the AVEVA token envelope already observed on the client side: one round byte, a 4-byte little-endian token length, then SSPI token bytes. On the first round, `ProcessServerToken` calls helper `0x0050FFC0`, which locks the server context collection and inserts or refreshes a keyed native context object. It then calls helper `0x00517AB0` to look up that object. If no context object is returned, the server sets custom error code `0x29` and `ValCl` fails before any successful context registration. With a context object, helper `0x00505C00` calls Secur32 `AcceptSecurityContext` through the service import table at `0x005A0340`, treating both success and `SEC_I_CONTINUE_NEEDED` (`0x90312`) as valid protocol progress. Only after the terminal round does `HistoryService.ValidateClientCredential` add the context GUID to its managed context-handle collection. This confirms the remaining managed replay gap is before or inside server `ProcessServerToken` context setup/lookup, not `OpenConnection3` itself. A tighter IL window confirms the line number reported in System Platform logs is the catch/log path, not the exact null-reference instruction. The normal path is: `Guid.TryParse` on the WCF handle at IL `0x012A`, `CServerBuffer` allocation through a vtable call at `0x0183`, byte-array pointer/length copy into buffer offsets `+72/+76`, and `CServerNode.ProcessServerToken` at `0x01DC`. Only when the native call returns success with `continue == false` does IL `0x0311` add the parsed context GUID to `m_contextHandles`; when `continue == true`, the method returns the server token without final handle registration. This keeps the runtime server helper probe as the most direct remaining evidence target. Additional server disassembly and string decoding identify the native object as `aahClientAccessPoint::CServerContext`. The first-round setup helper uses the server lock at `CServerNode +0xE80` and keyed context map at `CServerNode +0xE98`, logs `Adding ServerContext 0x%p`, constructs a 0x3c-byte context object through helper `0x00505100`, and inserts the new node through a red-black-tree insertion helper at `0x0042F590`. The lookup helper reads the same map and returns the context object from map node offsets `+0x20/+0x24`; a null result is what drives the `0x29` custom error path. The credential helper calls `AcquireCredentialsHandleW` with the UTF-16 package string `Negotiate`. The token-processing helper is logged as `aahClientAccessPoint::CServerContext::ProcessClientToken`; its failure log string still says `InitializeSecurityContext`, but the import actually called by this server helper is Secur32 `AcceptSecurityContext`. `HistoryService.ValidateIntegratedCredentials` is a separate server path: its first instructions read `ServiceSecurityContext.Current.WindowsIdentity`. That explains the earlier `OpenConnection2`/`OpenConnection3` null-reference failures on bindings where `ServiceSecurityContext.Current` is absent. Those errors are evidence of selecting the wrong integrated-credential path, not a user/password validation result. A focused object-window capture now shows the successful native `GetInterfaceVersion` path populates the history proxy managed-pointer slot at `CHistoryConnectionWCF +608` and the ready flag at `+669`; between `GetInterfaceVersion` completion and `ValCl` entry, a second managed-pointer slot at `+616` is populated for the binding wrapper. Across both successful `ValCl` rounds the parent `CHistoryConnectionWCF` object window is stable, but the `+608` history proxy target mutates at bytes `96..101`; the `+616` binding target and `+640` Windows identity target remain stable. This points at native-managed proxy wrapper state as the next concrete evidence target. Static IL inspection of `CHistoryConnectionWCF.Initialize` narrows the meaning of the AVEVA log line that says `Initialize: DataSourceId()`. It is client-side connection/proxy setup, not a separate WCF `Initialize` operation: the method logs `Initialize`, retrieves or creates the managed `/Hist` proxy and binding through `InitializeProxy`, stores those pointers at the same `+608`/`+616` managed-pointer slots seen at runtime, then does the same for the `/Trx` proxy. The history proxy initializer has only the previously mapped WCF setup branches: `ConfigurePipeProxy` at IL `0x0098` and `ConfigureTcpProxy` at IL `0x038E`, followed by `SetProxyString`. The decompiled `HistoryServiceContract` interfaces contain no `Initialize` operation contract. This makes the log line supporting evidence for local proxy setup state, but not a missing service call that a managed replay can simply add before `ValCl`. A focused server-side Frida probe was added at `scripts\frida\aahclientaccesspoint-valcl-context.js` with runner `scripts\Capture-AahClientAccessPointValClContext.ps1`. It hooks `ProcessServerToken`, the first-round context setup and lookup helpers, and the `AcceptSecurityContext` wrapper while logging only sanitized pointer, GUID, round, length, and return-value metadata. The script writes a `.frida.log` sidecar. An elevated PowerShell session (Admin, High Mandatory Label, `SeDebugPrivilege` enabled, `SeImpersonatePrivilege` enabled) ran both scenarios on `2026-05-03` and Frida attach was still rejected with the CLI message `Failed to attach: process with pid either refused to load frida-agent, or terminated during injection`. Direct `frida.attach()` from the Python API reveals the actual exception class is `frida.ProcessNotRespondingError`, which means the agent injection handshake did not complete in time, not that the OS refused the DLL load. The original suspected cause (mitigation policy `MicrosoftSignedOnly` / `BlockNonMicrosoftBinaries`) is now disproven: `Get-ProcessMitigation -Id ` reports every category OFF for this process, including `BinarySignature.MicrosoftSignedOnly`, `DynamicCode.BlockDynamicCode`, `Cfg.Enable`, `ImageLoad.BlockRemoteImageLoads`, `ExtensionPoint.DisableExtensionPoints`, and `UserShadowStack.*`. Process access from the elevated token also succeeds at `PROCESS_ALL_ACCESS`, including `PROCESS_VM_OPERATION`, `PROCESS_VM_WRITE`, and `PROCESS_CREATE_THREAD`, so the DACL is not blocking injection. Cross-bitness Frida (64-bit Python attaching to a fresh `C:\Windows\SysWOW64\cmd.exe`) attaches and runs scripts cleanly, so the WOW64 path itself is not broken. Defender real-time protection, behavior monitoring, and on-access protection are all OFF, no third-party AV/EDR product is registered with `SecurityCenter2`, no EDR-style filter driver is active, no `frida` modules appear in the target's loaded module list before or after a failed attempt, no IFEO debugger entry exists for `aahClientAccessPoint.exe`, and `AppInit_DLLs` is empty in both 64-bit and WOW64 hives. Attach attempts with `realm='native'`, `realm='emulated'`, and `persist_timeout=30` all fail identically. The remaining likely cause is service-internal: `aahClientAccessPoint.exe` runs 152 threads, many in `EventPairLow` ALPC/SCM waits, and Frida's in-memory manual-mapper agent does not get a cooperative thread for its RPC bootstrap. This is consistent with `ProcessNotRespondingError` rather than a load-time rejection. The NativeRead probe still completed end-to-end with the canonical fixture row (`TagKey=238`, `Value=0`, `Quality=133`, `OpcQuality=192`, `QualityDetail=248`) but emitted no server-side helper events. The ManagedValCl probe ran the .NET Framework named-pipe ValCl path against `net.pipe://localhost/Hist`, reproduced the canonical first wrapped NTLM envelope (raw outgoing 64 bytes, wrapped 69 bytes, wrapped prefix `01400000004e544c4d5353500001000000b7b218e209000900370000000f000f`), and again returned `ServerSuccess=false`, `ServerOutputLength=0`, `ErrorLength=5` with `NativeError {Type:4, Code:1, Name:Failure}` — matching prior managed named-pipe ValCl failures and confirming the failure shape is reproducible but providing no new server-side helper visibility. None of the five diagnostic questions (whether `0x0050FFC0` ran, whether `0x00517AB0` returned a context, whether `AcquireCredentialsHandleW` succeeded, whether `AcceptSecurityContext` was reached, whether failures are pre- or post-context-map insertion) can be answered from these captures. ETW SSPI tracing on `2026-05-03` produced server-helper-boundary evidence without injection. A `logman` trace session capturing the `LsaSrv {199FE037-2B82-40A9-82AC-E1D46C792B99}`, `LSA {CC85922F-DB41-11D2-9244-006008269001}`, `Microsoft-Windows-NTLM {AC43300D-5FCC-4800-8E99-1BD3F85F0320}`, `NTLM Security Protocol {C92CF544-91B3-4DC0-8E11-C580339A0BF8}`, and `Security: NTLM Authentication {5BBB6C18-AA45-49B1-A15F-085F7ED0AA90}` providers at level `0xFF` and keywords `0xFFFFFFFFFFFFFFFF` recorded: | Run | Total events | aahClientAccessPoint events | lsass events | |---|---|---|---| | NativeRead (success) | 5610 | **10** | 4330 | | ManagedValCl (fail) | 133 | **0** | 121 | Successful native server-side activity is a 47-millisecond burst of legacy MOF events (Ids `30, 34, 35, 40, 84, 10, 12, 16, 17, 86`) inside PID `aahClientAccessPoint`. The failing managed run produces zero events from the server PID at all — the server never reaches any SSPI helper invocation. lsass activity is also 35x lower in the failing run, consistent with auth never completing the first round end-to-end. This pins the previously logged `HistoryService.ValidateClientCredential caught System.NullReferenceException at line 1593` to a code path **before** `CServerNode.ProcessServerToken` at IL `0x01DC`. Static IL inspection of the full `HistoryService.ValidateClientCredential` body (token `0x06000774` in mixed-mode `aahClientAccessPoint.exe`, 779 instructions, dnlib output preserved at `artifacts/reverse-engineering/server-historyservice-validateclientcredential-il-latest.json`) enumerates every NRE-capable instruction reached on the straight-line happy path before the ProcessServerToken call: - **Prologue (IL `0x0000..0x0129`).** Constructs a `std::wstring`, case-insensitively compares two wide strings via `_wcsicmp` (`0x00A0`), and calls `CServerNode.LogHistorianMessage(this, _, CServerClient*, _, _, _, _)` at `0x00ED`. The third argument is a `CServerClient*`. If that pointer is derived from `OperationContext.Current` or related WCF context state and is null for the calling binding shape, the call site itself is an NRE candidate before `Guid.TryParse` is reached. - **Guid parse and recover (IL `0x012A..0x015E`).** `Guid.TryParse` on `arg.1` at `0x012A`. False branch raises custom error code `28` via `SError.SetToCustomError` and returns. True branch calls `::PtrToStringChars` at `0x0150` then `::GuidFromString` at `0x0159` to recover a native `_GUID` for ProcessServerToken. `Guid.TryParse` cannot NRE on a non-null string; the recovery path only NREs if the string is null, which a successful `TryParse` rules out. - **Allocator vtable chain (IL `0x0160..0x0183`).** Loads `&g_ClientAccessPoint + 2328`, dereferences once at `0x0178` to get the allocator pointer `pAllocator`, then derefs `*pAllocator` at `0x017E` (vtable pointer) and `*(vtable + 40)` at `0x0182` (slot for the allocator method), and `calli` at `0x0183` returns `CServerBuffer*`. **NRE candidates: `0x017E` if the field at `g_ClientAccessPoint + 2328` is null/uninitialised, or `0x0182` if the vtable is malformed.** Both target a process-wide static, so they would fail identically for the successful native path; rule out unless the field is initialised lazily per-binding. - **Allocator-null branch (IL `0x0189..0x01A3`).** `brtrue.s` on the returned `CServerBuffer*`. Null path raises custom error code `204` ("No more buffer") and returns false. So a null allocator result is handled and not the NRE source. - **Byte-array copy (IL `0x01A8..0x01C6`).** `ldelema System.Byte` on `arg.2` at `0x01AA` and `ldlen` on `arg.2` at `0x01B2`. **Both NRE if the WCF-deserialised `inputBuffer` parameter is null.** The two pointer/length values are then `stind.i4`'d into `*(CServerBuffer + 72)` and `*(CServerBuffer + 76)`, matching the documented `+72/+76` offsets. - **ProcessServerToken call (IL `0x01CA..0x01DC`).** Loads `&(g_ClientAccessPoint + 2144)` as the `CServerNode*` `this`, the parsed `_GUID`, the `CServerBuffer*`, a `ref bool continue`, and a `ref SError`, then calls `ProcessServerToken` (token `0x0600064F`). The IL slice does not include exception-handler metadata (current `dnlib-method` output covers instructions only), so the precise source-line `1593` reported by the System Platform log catch handler cannot be mapped to one specific instruction from static IL alone. The structural narrowing is: the throw must happen at a `ldelema`, `ldlen`, `ldind`, or `calli` instruction reached before `0x01DC`. The shortlist is `0x00ED` (LogHistorianMessage with `CServerClient*`), `0x017E` and `0x0182` (allocator vtable derefs), and `0x01AA` / `0x01B2` (byte-array deref). All other instructions in the window either operate on managed strings already proven non-null or on static-field addresses. Differential analysis against the successful native path narrows this further. `g_ClientAccessPoint` is a process-wide singleton, so the `+2328` field has the same value for both runs; the vtable chain is therefore unlikely to be the differentiator. The native wrapper's successful local read uses the same `Security.Mode = None` uncompressed pipe binding the managed probe uses, so `ServiceSecurityContext.Current.WindowsIdentity` is identical in both paths and the prologue `CServerClient*` derivation is also unlikely to be the differentiator. **The remaining structural difference is the WCF parameter binding: if the managed probe's SOAP body schema causes WCF to deserialise `inputBuffer` as null even though a 69-byte wrapped token is on the wire, `ldelema` at `0x01AA` would NRE.** The managed probe sends 69 bytes per its sanitised log, but the schema expectation on the server side has not been verified end-to-end. The next concrete evidence target is therefore not more static IL, but a SOAP-body comparison: dump the actual `` element the .NET Framework probe writes versus the wire shape the native wrapper writes for `/Hist-Integrated.ValCl`. If the schemas differ, the WCF service deserialises `arg.2` as null and the IL window decisively fails at `0x01AA`. If the schemas match, the throw is earlier (the prologue's `CServerClient*` log argument becomes the prime suspect) and runtime confirmation needs PsExec SYSTEM injection or a signed Detours stub at IL `0x00ED`. SOAP-body comparison on `2026-05-04` resolved this. Enabling `` in `AVEVA.Historian.NetFxWcfProbe.exe.config` and capturing the on-the-wire SOAP body for the failing `aa/Hist/ValCl` request produced: ```xml ...GUID... AUAAAABOVExNU1NQAAEAAAC3... ``` The 69-byte wrapped NTLM token IS on the wire as base64 inside ``. The matching server response, however, used `` rather than the expected `` shape, exposing a parameter-name mismatch. `ildasm` against `aahClientAccessPoint.exe` confirmed the actual server contract is ```il ValidateClientCredential(string handle, uint8[] inBuff, [out] uint8[]& outBuff, [out] uint8[]& errorBuffer) ``` not `inputBuffer` / `outputBuffer`. WCF builds the request body element name from the C# parameter name, so the probe sent `` and the server's WCF deserialiser ignored that unknown element, leaving `arg.2 = inBuff = null`. IL `0x01AA` `ldelema System.Byte` then NREs, which the C++/CLI catch handler at HistoryService.cpp line 1593 maps to native error `Type=4 Code=1 Failure` with an empty error buffer. Adding `[MessageParameter(Name = "inBuff")]` and `[MessageParameter(Name = "outBuff")]` to the probe's `ValidateClientCredential` declaration and re-running unblocks the request: round 0 returns `ServerSuccess=true`, `ServerOutputLength=239`, `ServerContinue=true`, with `ServerOutputPrefixHex` `014e544c4d535350000200...` (a `0x01` continue byte followed by NTLMSSP type-2 challenge). This matches the previously documented native-success "69-byte client input to 239-byte server output" exactly. Round 1 then sends the 88/93-byte NTLMSSP type-3 token and the server returns `Type=129 Code=0x80090308` (`SEC_E_INVALID_TOKEN`) with a 100-byte error buffer whose ASCII payload includes `aahClientAccessPoint::CServerContext::ProcessClientToken` and `InitializeSecurityContext`. That is a real SSPI-level rejection inside `AcceptSecurityContext`, not the previous parameter-binding NRE — the original blocker is gone and the next layer of failure is exposed. The same fix is now applied to the production SDK contracts `IHistoryServiceContract2.ValidateClientCredential` and `IStorageServiceContract.ValidateClientCredential` via `[MessageParameter(Name = "inBuff" | "outBuff")]`, preserving the conventional C# parameter names while making WCF emit the server-correct element names. `ildasm` against `aahClientAccessPoint` also revealed several other operations the SDK does not yet need (`EnsT` `InBuff/OutBuff`, `EnsT2` `InBuff/OutBuff`, `RTag2` `pInBuff/outBuff`, `ExKey` `inBuff/OutBuff`, `StJb` `strJobid`, `GtJb` `strJobid/jobstatus`) carry the same parameter-naming mismatch with their current SDK declarations. Those are intentionally left alone for this read-only pass; audit them when their flows become required. The next concrete evidence target is now `SEC_E_INVALID_TOKEN` on `ValCl` round 1: native traces showed `InitializeSecurityContextW` with request flags `0x2081C` for the first round and `0x81C` for later rounds. The .NET Framework probe uses default flags through the SSPI wrapper. Replicating those exact flags (and confirming the Negotiate target name matches the wrapper's `NT SERVICE\aahClientAccessPoint`) is the next testable hypothesis. Native SSPI flag replication on `2026-05-04` resolved this. Decoding the documented native flags: - `0x2081C` (round 0) = `ISC_REQ_IDENTIFY (0x20000) | ISC_REQ_CONNECTION (0x800) | ISC_REQ_CONFIDENTIALITY (0x10) | ISC_REQ_SEQUENCE_DETECT (0x8) | ISC_REQ_REPLAY_DETECT (0x4)` - `0x81C` (round 1+) = same minus `ISC_REQ_IDENTIFY` The probe's `SspiClient` previously used `ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY | ISC_REQ_CONNECTION = 0x10910`, which is missing `ISC_REQ_REPLAY_DETECT`, `ISC_REQ_SEQUENCE_DETECT`, and round-0 `ISC_REQ_IDENTIFY`. The REPLAY/SEQUENCE pair gates NTLM MIC (Message Integrity Code) generation in the type-3 response message; without them the type-3 message has no MIC and the server's `AcceptSecurityContext` rejects it with `SEC_E_INVALID_TOKEN`. Adding `ISC_REQ_REPLAY_DETECT`, `ISC_REQ_SEQUENCE_DETECT`, and round-0-only `ISC_REQ_IDENTIFY` (keeping `ISC_REQ_ALLOCATE_MEMORY` for buffer convenience and tracking the round count internally in `SspiClient`) reproduces the documented native ValCl sequence byte-for-byte from a fully managed client: | Round | Outgoing wrapped | Server output | ServerContinue | NativeError | |---|---|---|---|---| | 0 | 69 bytes | 239 bytes (NTLM type-2 challenge) | true | none | | 1 | 93 bytes | **1 byte (`0x00` terminal)** | false | **none** | `FinalServerSuccess: true`, `FinalNativeError: null`. This matches the previously documented "successful native two `ValCl` rounds: 69-byte client input to 239-byte server output, then 93-byte client input to one-byte terminal output" exactly. The long-standing managed `ValCl` blocker is therefore resolved. The chain that successful native reads use is now reproducible from a managed client end-to-end: 1. `Hist-Integrated.GetV` → version `11` 2. `Hist-Integrated.ValCl` round 0 (69 → 239 bytes) ✓ 3. `Hist-Integrated.ValCl` round 1 (93 → 1 byte terminal) ✓ The next steps in the chain — `OpenConnection3` (with the now-known context key), `Retr.IsOriginalAllowed`, and `Retr.StartQuery2` — should now be exercisable without server-side helper failures, because the prerequisite native context-map registration that `ProcessServerToken` performs has finally been completed by a managed client. The production SDK currently has no SSPI client (only the wrap/unwrap helpers in `HistorianWcfAuthenticationProtocol`). When the SDK auth flow is wired up for the production read path, it must use the same native-equivalent flags. .NET 10's `System.Net.Security.NegotiateAuthentication` does not expose `ISC_REQ_*` directly, so the SDK will likely need to P/Invoke `InitializeSecurityContextW` (or equivalent) to set `IDENTIFY` + `REPLAY_DETECT` + `SEQUENCE_DETECT` exactly. Sample reference implementation in `tools/AVEVA.Historian.NetFxWcfProbe/Program.cs` `SspiClient`. End-to-end chain verification on `2026-05-04`. With the WCF parameter fix and SSPI flag fix in place, the .NET Framework probe was extended to chain `Hist.Open2` (replaying the captured 1346-byte v6 request with the leading 16 context-key bytes spliced to match the managed `ValCl` GUID), then `Retr.IsOriginalAllowed`, then `Retr.StartQuery2` (replaying the captured 251-byte `OtOpcUaParityTest_001.Counter` `DataQueryRequest`). The result: | Step | Outcome | |---|---| | `Hist.GetV` | version `11` | | `Hist.ValCl` round 0 | 239-byte server response, NTLM type-2 challenge | | `Hist.ValCl` round 1 | 1-byte terminal response | | `Hist.Open2` | **42 bytes, version `0x03`, transient `/Retr` client handle decoded** | | `Retr.GetV` | version `4` | | `Retr.IsOriginalAllowed(handle)` | return code `0`, `isAllowed = true` | | `Retr.StartQuery2(handle, 1, 251 bytes, ...)` | **`Success=true`, response 31 bytes, `QueryHandlePresent=true`, no error** | The 31-byte `StartQuery2` response SHA-256 `4c062b5ce8181308f0f46bfd8c6088acb52e6ade94401651b7d3ccc8952edfb5` is **byte-for-byte identical** to the previously captured native success response (recorded in the existing `Wcf.StartQuery2` block of `Current Hard Blocker` and in `instrumented-openconnection3-correlation/capture.ndjson`). The full AVEVA Historian native wire protocol chain through `StartQuery2` is now reproducible end-to-end from a fully managed client. This required one additional contract fix: `IRetrievalServiceContract2` also had the parameter-name mismatch class of bug. The actual server contract uses `pRequestBuff` / `pResponseBuff` / `errSize` / `err` on `StartQuery2` (and `pResultBuff` / `errSize` / `err` on `GetNextQueryResultBuffer2`, `errSize` / `err` on `EndQuery2`); the SDK declared them as `requestBuffer` / `responseBuffer` / `errorSize` / `errorBuffer`. `[MessageParameter(Name = ...)]` attributes added to `src/AVEVA.Historian.Client/Wcf/Contracts/IRetrievalServiceContract2.cs`. Replay-only details: the `Open2` body was constructed by reading the captured 1346-byte v6 native request from `artifacts/reverse-engineering/openconnection3-request-replay.bin` and overwriting bytes `1..16` with the new managed-side context-key GUID; the 251-byte data query was loaded as-is from `artifacts/reverse-engineering/startdataquery-request-replay.bin`. Both inputs and the captured native fields they contain (machine name, process name, etc.) are local to the dev host. The probe's stdout JSON only echoes lengths, SHAs, version bytes, and prefix hex; it does not echo identity payloads. The next concrete step is the production-SDK pass to wire the managed auth chain: implement an SSPI client that emits the native flags, replace the captured-replay `Open2` payload with a schema-driven serialiser using `HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6` (already in the SDK), and chain `ValCl → Open2 → /Retr.StartQuery2 → /Retr.GetNextQueryResultBuffer2` for the canonical read fixture. The reverse-engineered protocol is now fully understood end-to-end for the read path; remaining work is plumbing. Production SDK plumbing landed on `2026-05-04`. The full managed read path is now wired and verified end-to-end against the live local Historian: - `src/AVEVA.Historian.Client/AVEVA.Historian.Client.csproj` — added `System.ServiceModel.NetNamedPipe 10.0.652802` package. - `src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs` — added `CreateMdasNetNamedPipeBinding` (Security.Mode = None + MDAS encoder wrapper) and `CreatePipeEndpointAddress`. Marked `[SupportedOSPlatform("windows")]` since named pipes are Windows-only. - `src/AVEVA.Historian.Client/Wcf/HistorianSspiClient.cs` (new) — P/Invoke `InitializeSecurityContextW` / `AcquireCredentialsHandleW` / `DeleteSecurityContext` / `FreeCredentialsHandle` with internal round counter and the canonical native flag bitmasks (`0x2081C` round 0 / `0x81C` later, plus `ALLOCATE_MEMORY` for buffer convenience). Constants exposed as `internal` for test verification. - `src/AVEVA.Historian.Client/Wcf/HistorianDataQueryProtocol.cs` — added `TryParseGetNextQueryResultBufferRows` for the raw/Full row layout: 6-byte buffer header (`UInt16 version=9`, `UInt32 rowCount`) followed by `rowCount` self-describing rows of `UInt32 tagKey + UInt32 tagNameChars + tagName UTF-16 + UInt32 sampleBlockCount + Int64 startUtcFileTime + UInt32 quality + UInt32 qualityDetail + UInt32 opcQuality + Double numericValue + Double percentGood + 1-byte marker + 34 trailing bytes`. The 5-byte error/terminal `04 1E 00 00 00` (type 4, code 30 = "no more data") is recognised as the "stop looping" signal. - `src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs` (new) — chains `Hist.GetV → Hist.ValCl × N → Hist.Open2 → /Retr channel → Retr.GetV → Retr.IsOriginalAllowed → Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2`. Builds the OpenConnection3 v6 request through `HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6` with the documented native constants (`ClientType=4`, `ConnectionMode=0x402`, `FormatVersion=4`, `HcalVersion=17`, `DataSourceId=ClientDllVersion="2020.406.2652.2"`, `ClientCommonInfo.ClientVersion=999_999`, `ShardId=Guid.Empty`) and a 1026-byte zero credential block. - `src/AVEVA.Historian.Client/HistorianClientOptions.cs` — added `Transport` (defaults to `LocalPipe`) and `TargetSpn` (defaults to `NT SERVICE\aahClientAccessPoint`). - `src/AVEVA.Historian.Client/HistorianTransport.cs` (new) — enum `LocalPipe` / `RemoteTcpIntegrated` / `RemoteTcpCertificate`; only `LocalPipe` is implemented in this pass. - `src/AVEVA.Historian.Client/Models/HistorianSample.cs` — added `PercentGood` positional property. - `src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs` — constructor now takes `HistorianClientOptions`; `ReadRawAsync` delegates to `HistorianWcfReadOrchestrator` on Windows + `Transport.LocalPipe`, throws `ProtocolEvidenceMissingException` otherwise. - `tests/AVEVA.Historian.Client.Tests/` — new `HistorianSspiClientTests` (5 flag-selection tests), `WcfBindingFactoryTests` (3), `WcfDataQueryResultBufferTests` (5 golden-byte parser tests using the captured 570-byte fixture). `HistorianClientIntegrationTests.ReadRawAsync_AgainstLocalHistorian_ReturnsAtLeastOneRow` (gated on `HISTORIAN_HOST=localhost` + `HISTORIAN_TEST_TAG`) exercises the full managed chain against the live local Historian. Test results with `HISTORIAN_HOST=localhost` and `HISTORIAN_TEST_TAG=OtOpcUaParityTest_001.Counter`: **69/69 pass**, including the live read. Without the env vars, the integration test skips cleanly and 64/64 pass. The reverse-engineering phase for the read path is complete. The production SDK now reads history end-to-end against the live local Historian using only managed code — no `aahClientManaged.dll` or `aahClient.dll` loaded in the consuming process. Aggregate (`Interpolated` / `TimeWeightedAverage`) read modes, remote TCP transport, explicit username/password auth, event reads, and other contracts (`EnsT`, `RTag2`, `ExKey`, `StJb`, `GtJb` — all of which ildasm shows have the same parameter-naming mismatch as the resolved `ValCl` / `StartQuery2` operations) remain follow-up work but no longer face protocol-discovery blockers — only the parameter-rename audit + per-mode row-layout decoding + transport-binding additions. All listed follow-up work landed on `2026-05-04`. The SDK now supports: `ReadRawAsync`, `ReadAggregateAsync`, `ReadAtTimeAsync`, and `ReadEventsAsync` — all verified against the live local Historian (72/72 tests pass with the integration env vars set). Specifically: - **`[MessageParameter]` audit (Phase B2):** `ildasm` against `aahClientAccessPoint.exe` was used to verify server parameter names for every operation across `IHistoryServiceContract`, `IHistoryServiceContract2`, `IRetrievalServiceContract`, `IRetrievalServiceContract3`, and `IRetrievalServiceContract4`. `[MessageParameter(Name = ...)]` attributes were applied to ~30 parameter-name mismatches: `EnsT`, `EnsT2`, `RTag2`, `ExKey`, `AddTEx`, `DelTep`, `StJb`, `GtJb`, `AddS2`, `UpdC3`, `DelT`, `VldC2`, `OpenConnection`, `AddTags`, `RegisterTags`, `AddStreamValues`, `ValidateClient`, `UpdateClientStatus`, `SetClientTimeOut`, `CloseConnection`, `StartQuery`, `GetNextQueryResultBuffer`, `ExecuteSqlCommand`, `GetRecordSetByteStream`, `StartTagQuery`, `QueryTag`, `StartEventQuery`, `GetNextEventQueryResultBuffer`, `EndEventQuery`, `GetShardTagidsByTagnameAndSource`. Build clean, 72/72 tests pass. - **Aggregate row parser (Phase B4):** `HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferAggregateRows` parses the same wire shape as raw rows but interprets FILETIME #1 (at row offset `8 + tagNameLen*2 + 4`) as the interval EndTimeUtc and the FILETIME at trailer offset 2 (row offset `8 + tagNameLen*2 + 43`) as StartTimeUtc — derived from native struct evidence (`+0x28 = EndDateTime`, `+0x150 = StartDateTime`) and the captured raw fixture where Start == End. The orientation was verified by the live `ReadAggregateAsync` test against `OtOpcUaParityTest_001.Counter` returning consistent `TimeWeightedAverage` rows. - **Aggregate + at-time wiring (Phase B5):** `HistorianWcfReadOrchestrator.ReadAggregateAsync` and `ReadAtTimeAsync` chain `Hist.GetV → ValCl × N → Hist.Open2 → Retr.IsOriginalAllowed → Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2`. The aggregate request maps the public `RetrievalMode` enum to the documented `HistorianDataQueryRequest.QueryType` values (`Full → 2`, `Interpolated → 3`, `TimeWeightedAverage → 5`, `Cyclic → 4`); other modes throw `ProtocolEvidenceMissingException` until they have a fixture- backed mapping. `ReadAtTimeAsync` issues a one-tick `Interpolated` window per requested timestamp and folds each aggregate result into a `HistorianSample`. - **Event flow (Phase B6+B7+B8):** `HistorianWcfEventOrchestrator` mirrors the read orchestrator but targets `IRetrievalServiceContract4.StartEventQuery` and `GetNextEventQueryResultBuffer`. The chain reaches `StartEventQuery` successfully — a real victory, since the previous probe attempts failed at this exact call site. `GetNextEventQueryResultBuffer` then returns native error type=4 code=85 (0x55), which is a NEW server response (not the canonical `code=30` "no more data"). The orchestrator treats any 5-byte type=4 error buffer as a soft terminal and surfaces the full code via the `LastErrorBufferDescription` diagnostic. Likely investigation targets for code 85: the existing notes describe a native `CreateDefaultEventTag` step that calls `RegisterTags2` to register a synthetic `CM_EVENT` tag before any event read can return rows; we currently skip that prerequisite. Event-row WCF wire format also remains undecoded (only native struct snapshots are captured), so even if rows came back we'd need a fresh capture to parse them. Both of these are documented as open follow-ups. - **Remote TCP transport (Phase B1 + B9):** `HistorianWcfBindingFactory.CreateBindingPair(options)` selects binding + endpoint pairs by `HistorianTransport`: `LocalPipe` → `net.pipe://host/Hist` + `/Retr` (existing pipe binding); `RemoteTcpIntegrated` → `net.tcp://host:port/Hist-Integrated` (NetTcpBinding + Windows transport security) for the auth chain plus plain MDAS Net.TCP `/Retr` for queries (per existing evidence that `/Retr` rejects Windows transport security); `RemoteTcpCertificate` → `/HistCert` over MDAS+certificate + plain `/Retr`. The orchestrators now consume the binding pair transport-agnostically. Untested against a live remote Historian on this host, but the auth chain is ready to fire. - **Explicit username/password auth (Phase B3):** `HistorianSspiClient` has a second constructor overload that builds a `SEC_WINNT_AUTH_IDENTITY` Unicode struct and passes it as `pAuthData` to `AcquireCredentialsHandleW`. The auth chain helper picks the constructor based on `HistorianClientOptions.IntegratedSecurity`: when `false` and `UserName` is set, it parses `DOMAIN\user` (or treats the value as a bare user) and forwards it with `Password`. Untested against a live remote Historian; reserved for the explicit-creds production path. Verified test results with `HISTORIAN_HOST=localhost` and `HISTORIAN_TEST_TAG=OtOpcUaParityTest_001.Counter`: **72/72 pass (15-second total)**, including the four live integration tests (`ProbeAsync`, `BrowseTagNamesAsync`, `GetTagMetadataAsync`, `ReadRawAsync_AgainstLocalHistorian`, `ReadAggregateAsync_AgainstLocalHistorian`, `ReadAtTimeAsync_AgainstLocalHistorian`, `ReadEventsAsync_AgainstLocalHistorian`). Without env vars, all tests skip cleanly to 72/72. Remaining open work (no protocol discovery — pure plumbing or fresh capture): - Decode the event-row WCF wire format (no captured fixture yet). Most direct path: instrument the native trace harness to capture `Wcf.GetNextEventQueryResultBuffer.ResultBytes` while running the successful native event read, base-64-encode into the existing capture.ndjson, then write a parser using the same approach as the data-query parser. - Investigate code 85 (`0x55`) terminal from `GetNextEventQueryResultBuffer`. Most likely the missing `RegisterTags2(CM_EVENT)` prerequisite. See `wcf-register-event-tag-latest.json` for the prior probe attempts and the documented `ConvertEventTagToTagMetadata` GUID values (`353b8145-5df0-4d46-a253-871aef49b321` event tag id, `5f59ae42-3bb6-4760-91a5-ab0be01f2f27` event type id). - Verify remote TCP transports against an actual remote Historian. Both `RemoteTcpIntegrated` (use `/Hist-Integrated`) and `RemoteTcpCertificate` (use `/HistCert`) are wired but unverified on this host. - Verify explicit username/password against a live Historian with a non-current user account. - Add `RetrievalMode` mappings beyond the four currently supported (`Cyclic`, `Delta`, `BestFit`, `MinimumWithTime`, `MaximumWithTime`, `Integral`, `Slope`, `Counter`, `ValueState`, `RoundTrip`, `StartBound`, `EndBound`) when corresponding native QueryType constants are documented. - Decode the trailing 24 bytes of each row body (after the EndTime/Quality/NumericValue/PercentGood/marker/StartTime stack); they're zero in the captured fixture but vary in some rows (e.g. trailer bytes at row+125..+132 differ across the 4 captured rows for the same tag), suggesting a per-sample value or source identifier we haven't decoded. Event-flow follow-up investigation on `2026-05-04`. The event orchestrator now uses `ConnectionMode = 0x501` (Event) so the chain reaches `Retr.GetNextEventQueryResultBuffer`. The server returns native error type=4 code=85 (`0x55`) with zero rows. SQL ground truth (`SELECT * FROM Runtime.dbo.Events`) confirms 5+ events ARE available in the test window — they're just not flowing because the session is not subscribed. Attempted fix: call `IHistoryServiceContract2.RegisterTags2` with a `count=1 + 16-byte CM_EVENT GUID` body before `StartEventQuery`. Tested with both `storageSessionId` and `contextKey` as the handle parameter, in both upper- and lower-case GUID-D format. Every variant returned native error code=51 (InvalidParameter). Reverted. Per existing notes (lines 673–728 in this doc), the actual native prerequisite is `IHistoryServiceContract.AddTags` (`AddT`) with a `CTagMetadata` payload (`version=1 byte + optional-mask=2 bytes + data-type-byte=10 + tag-id=16 bytes + compact UTF-16 strings`), NOT `RegisterTags2`. Documented CM_EVENT identity: tag id `353b8145-5df0-4d46-a253-871aef49b321`, event type id `5f59ae42-3bb6-4760-91a5-ab0be01f2f27`, `CDataType=10`, storage type `2`. The remaining concrete next step for live event reads: 1. Instrument `Wcf.AddT.Request` on a running native event harness to capture the exact `CTagMetadata` wire bytes. The existing reverse-engineering CLI has IL-rewrite instrumentation that captures other WCF request/response bodies — extend the same approach to AddT. 2. Wire the captured payload into `HistorianWcfEventOrchestrator` as the `additionalSetup` callback for the event chain. 3. Once `AddT(CTagMetadata)` succeeds, capture the resulting `Wcf.GetNextEventQueryResultBuffer.ResultBytes` and write a parser similar to the data-query row parser. Until step 1 lands, `ReadEventsAsync` reaches the chain layer successfully but returns empty results. The diagnostic helper `EventChainDiagnosticTests.EventOrchestrator_DiagnosticDump_AgainstLocalHistorian` surfaces `LastResultBufferLength` and `LastErrorBufferDescription` via `ITestOutputHelper` for iteration. Raw ETL files contain SSPI tokens, machine names, and identity metadata; they stay under `artifacts/reverse-engineering/etw-sspi-{nativeread,managedvalcl}-*.etl` and are not committed. Therefore "rerun from elevated PowerShell" is no longer a viable next step on this host. Practical alternative evidence paths for the server helper layer: - SYSTEM-level injection from an interactive PsExec session (`PsExec64 -accepteula -s -i frida -p -l