Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).
QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.
Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
*mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.
Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).
Milestone 0 (full gRPC parity) is complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
3.4 KiB
R0.1 browse over gRPC — StartTagQuery takes an OData filter (2026-06-21)
Live-probed RetrievalService.StartTagQuery / QueryTag against a real 2023 R2 server over the
gRPC front door (string-handle = uppercase Open2 storage GUID). Key result: browse is feasible on
2023 R2 — the 2020 WCF "metadata-server pipe" wall does not block here.
StartTagQuery — CRACKED
StartTagQuery(strHandle, btRequest) where btRequest = the native
marker(26449) + version(1) + WriteHistorianString(filter) buffer
(HistorianTagQueryProtocol.CreateStartTagQueryAttempt). The server runs
CMdServer::StartTagQuery::StartActiveTagnamesQuery over \\.\pipe\aahMetadataServer\console and
parses the filter string as OData (not SQL-LIKE). Swept filters:
| filter | result |
|---|---|
startswith(TagName,'Sys') |
✅ success, 8-byte response |
contains(TagName,'Sys') |
✅ success |
TagName eq 'SysTimeSec' |
✅ success |
| `` (empty) | ✅ success (all tags) |
Sys* / * |
❌ ODataFilter ... bad token |
TagName like 'Sys%' / Name like 'Sys%' |
❌ rejected |
Success response btResponse is the 8-byte (queryHandle:uint, tagCount:uint) pair
(ParseStartTagQueryResponse). Live: startswith(TagName,'Sys') → tagCount = 220.
Implication for the public API: browse must translate the SDK's glob filter to OData —
* → empty, Pre* → startswith(TagName,'Pre'), *sub* → contains(TagName,'sub'),
exact → TagName eq '...'. (Escaping single-quotes in names still TBD.)
QueryTag — CRACKED (2026-06-21), browse SHIPPED
QueryTag(strHandle, uiQueryHandle, btRequest) is the paging call that returns the tag-name rows.
The blocker was the packet id: every guessed btRequest returned native error type 4 / code 72 =
InvalidPacketId (ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId). The generic
0x6751 header that StartTagQuery accepts is the wrong id for QueryTag.
How it was found (no Ghidra needed): a .rdata packet-descriptor table in
aahClientManaged.dll lists consecutive {uint marker, uint version} entries —
{0x6751, 1} (StartTagQuery) immediately followed by {0x6752, 1} (the paired op). Found by
pefile byte-scan of .rdata for 51 67 00 00 and dumping the surrounding dwords. Testing 0x6752
live confirmed it.
QueryTag wire format (live-verified):
- request
btRequest=u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count—queryType = 1returns tag-name rows (queryType = 0returns an empty/count-only page). - response
btResonse=u32 count + per-name(u32 charCount + UTF-16LE) + trailer(the trailer is the CloudHistorianNextIndex/TagMetadataBufferregion — ignored byHistorianTagQueryProtocol.ParseTagNameQueryPage). - Semantic fields match
ArchestrA.CloudHistorian.Contract.QueryTagRequest(QueryType/StartIndex/TagCount; the QueryHandle travels in the protobufuiQueryHandle).
Browse is shipped: HistorianClient.BrowseTagNamesAsync routes over gRPC when
Transport==RemoteGrpc via Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync
(StartTagQuery(OData) → paged QueryTag(0x6752) → EndTagQuery), with the SDK glob filter translated by
GlobToODataFilter. Golden-byte + glob unit tests and a gated live test
(BrowseTagNamesAsync_OverGrpc_ReturnsSystemTags) cover it. M0 gRPC parity is complete.