Deepened the R0.1 browse finding. QueryTag's constant rejection decodes to ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId (72): the btRequest needs a QueryTag-specific packet-id header (the generic 0x6751/v1 header StartTagQuery accepts is rejected). The semantic fields are known from CloudHistorian.Contract (QueryHandle/QueryType/StartIndex/TagCount request; TagNames[]+TagMetadataBuffer response), but the binary packet framing lives in native aahClient.dll — aahClientManaged.dll is mixed-mode (ilspycmd cannot decompile it) and no managed assembly builds the buffer. Finishing QueryTag needs native RE (Ghidra/IDA) or a live gRPC capture of the stock client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
3.2 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 — OPEN (needs native RE of aahClient.dll)
QueryTag(strHandle, uiQueryHandle, btRequest) is the paging call that returns the tag-name rows.
Every btRequest shape tried returns the constant native error type 4 / code 72 =
InvalidPacketId (ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId = 72; the empty
buffer alone gives a different code). So the btRequest must carry a packet-id header specific to
QueryTag that we don't have — the generic 0x6751/version-1 header (which StartTagQuery accepts) is
rejected here.
Semantic fields are known from ArchestrA.CloudHistorian.Contract:
- request
QueryTagRequest=QueryHandle:uint("q") + QueryType:ushort("t") + StartIndex:uint("s") + TagCount:uint("c") - response
QueryTagResponse=QueryHandle:uint + TagNames:string[]("t") + NextIndex:uint("i") + TagMetadataBuffer:byte[]("tb")— so QueryTag returns the names directly (plus an optional metadata buffer), and pages via StartIndex/NextIndex.
What's missing is the binary btRequest packet framing (the QueryTag packet id + how those fields
are laid out). That serializer lives in native aahClient.dll — aahClientManaged.dll is
mixed-mode (C++/CLI) so ilspycmd cannot decompile it, and no managed assembly builds the buffer.
Completing QueryTag therefore requires native RE (Ghidra/IDA on aahClient.dll) or a live gRPC
capture of the stock 2023 R2 client browsing. Do not ship a guessed QueryTag request (project
discipline: no guessed wire bytes).
Probe helpers live in Grpc/HistorianGrpcTagClient (ProbeStartTagQuery, ProbeTagQuerySequence)
and the gated StartTagQuery_OverGrpc_AcceptsODataFilter test pins the StartTagQuery+OData result.