Ship tag extended-property reads over the 2020 WCF aa/Retr/GetTepByNm op: HistorianClient.GetTagExtendedPropertiesAsync(tag) -> name/value pairs. String-handle op reached with the Open2 storage-session GUID formatted uppercase (same format that unlocked GETRP/GETHI/ExeC). Routed via the name-based native path (GetTagExtendedPropertiesByName, server-fetch flag), not the index-based TagQuery path. Evidence-backed findings from the capture: - GetTepByNm (and GetTgByNm) succeed with the uppercase handle -- further validates the resolved string-handle wall. - QTB (StartTagQuery) does NOT punch through: captured uppercase, it still fails server-side (CMdServer::StartActiveTagnamesQuery over the aahMetadataServer pipe) -- a metadata-server blocker, not handle format. - R1.6 (localized properties) has NO distinct op (only error-message/UI-text localization in the managed client); collapses into R1.5. Closed, not throwing. Wire format (golden-pinned, synthetic bytes -- no dev tag names committed): - request tagNames = uint count + per-name(uint charCount + UTF-16) - response = uint tagCount + per-tag(marker + compact-ASCII name + uint propCount + per-prop(marker + compact-ASCII name + 0x43 VT_BSTR value) + trailer); sequence-paged. Adds: HistorianTagExtendedProperty model, HistorianTagExtendedPropertyProtocol (codec), HistorianWcfTagExtendedPropertyClient (orchestration), dialect + public API; golden WcfTagExtendedPropertyProtocolTests (4) + gated live test (HISTORIAN_TEP_TAG). Tooling: Capture-TagExtendedProperties.ps1, decode-tag-properties-capture.py, harness tag-extended-properties scenario. Docs: wcf-tag-extended-properties.md; roadmap R1.5 DONE / R1.6 collapsed; wall doc + memory updated with the QTB-server-side nuance. 228 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
8.1 KiB
The 2020 WCF string-handle wall (2026-06-20)
✅✅ RESOLVED (2026-06-20): the "wall" was a handle-FORMAT bug, not a registration wall.
The string-handle ops are reachable from the pure-managed client after all. The Open2 storage-session GUID must be passed as the
string handleUPPERCASE, dash-separated, no braces —storageSessionId.ToString("D").ToUpperInvariant(). The earlier probes that "proved" the wall passed the GUID in .NET's default lowercaseToString("D"), which the server's session table does not match. Live-verified end-to-end against the local 2020 server:
- GETRP (R1.2) → returns the runtime
HistorianVersion(shipped).- GETHI (R1.4) →
returned=True, returns the version buffer (0C000000+ UTF-16 "20,0,000,000").- ExeC (R1.1) →
returned=True,Retr.GetVprime +ExeC("SELECT 1 AS ProbeValue", option=0)yieldsqueryHandle, thenGetR(handle, queryHandle, sequence=0)returns a 1232-byte result = a BinaryFormatter-serialized .NET DataTable (stream header…System.Data, Version=4.0.0.0…).Probes: gated
StringHandleProbeDiagnosticTests(GETHI + ExeC). Captures:scripts/Capture-RuntimeParam.ps1,scripts/Capture-ExecSql.ps1. The handle for ExeC/GetR is the same Open2 storage-session GUID (confirmed =outBuff[5..21]). The original analysis below is retained for history; treat its "blocked" conclusions as superseded — the only missing piece was the uppercase format.Update 2026-06-20 — R1.5
GetTepByNmshipped; QTB nuance.GetTagExtendedPropertiesFromName(GetTepByNm) is now shipped + live-verified with the uppercase handle (GetTagExtendedPropertiesAsync; seewcf-tag-extended-properties.md). It confirms the string-handle Retrieval family is reachable (andGetTgByNm/GetTagInfosFromName was observed succeeding alongside it). But not every string-handle op is just a format fix:QTB(StartTagQuery) was captured being sent with a correctly-uppercase handle and still failed witherror 1server-side (CMdServer::StartTagQuery::StartActiveTagnamesQueryover\\.\pipe\aahMetadataServer\console). So QTB/QTG (the active-tagnames query family) are blocked by the metadata server, not the handle format — distinct from the handle-format wall. R1.6 (localized properties) has no distinct op and collapses into R1.5.
Live-probing the local Historian 2020 (WCF, port 32568) for HCAL roadmap M1 surfaced a clean structural boundary on what the pure-managed client can call. It explains why R1.1/R1.4/R1.5 all fail and identifies the single RE target that unblocks the rest of the M1 read surface.
⚠️ Superseded — see the RESOLVED banner above. The boundary below is real only when the handle is sent lowercase. With the uppercased storage GUID the string-handle ops succeed.
The dichotomy
Retrieval/Status/History ops split by the type of their first (handle) parameter:
| Handle type | Examples | Status on 2020 WCF |
|---|---|---|
uint client handle (Open2 output) |
StartQuery2, GetNextQueryResultBuffer2, IsOriginalAllowed, GetTagInfosFromName/GetTagInfoFromName (GetTgByNm), GetSystemParameter, StartEventQuery, GetNextEventQueryResultBuffer, RegisterTags2, EnsureTags2, UpdateClientStatus3 |
✅ work — the proven read/browse/metadata/status-param/event/write surface |
string GUID handle |
ExecuteSqlCommand (ExeC), StartTagQuery (QTB), QueryTag (QTG), GetHistorianInfo (GETHI), GetTagExtendedPropertiesFromName (GetTepByNm), GetTagInfosFromName2 (GetTgByNm2), GetTagidsByTagnameAndSource |
⛔ blocked — native error type 4, code 51 (InvalidParameter) or 1 (Failure) |
Evidence (this probe + prior notes)
- ExeC → type 4 / code 51 for every handle variant (storageGuid, contextGuid).
Matches
implementation-status.md~982 / ~1404 ("StartTagQuery depends on earlier native session/filter registration … do not wire through guessed calls"). - GETHI (
HistorianVersionparam query — the exact native request shape fromBuildGetHistorianInfoRequest, withStat.GetV ×2priming) → type 4 / code 1 for all five handle formats tried: storage-session GUID, context GUID, uint as decimal, uint asX8hex, uint as0x-hex. In the only place GETHI is used (the event-priming chain) its result is wrapped inTryRunand discarded, so there was never evidence it actually returns data from the managed client. - GetTepByNm / QTB / QTG / GetTgByNm2 all take a
string handle→ same family.
Why
The string-handle ops are keyed off a native-side session/filter registration
that the C++ client performs but the managed replay does not reproduce. The uint
client handle is the Open2 session token the server already trusts; the string GUID
handle indexes a different per-service registration table that stays empty unless
the native priming is replicated faithfully. Stat.GetV ×2 alone is insufficient.
Consequence for the roadmap
Every remaining M1 read item is a string-handle op:
- R1.1
ExecuteSqlCommandAsync(ExeC) — blocked - R1.4
GetHistorianInfoAsync(GETHI) — blocked - R1.5 extended-property read (GetTepByNm) — blocked (string handle, confirmed)
- R1.6 localized-property read — same family
So M1 read-surface completion on 2020 WCF is gated entirely behind one RE target: the native session/filter registration for string-handle ops. Reverse-engineer it once and the whole family unlocks. Until then, the alternatives are:
- RE the registration — instrument the native
CRetrievalConnectionWCF/CStatusConnectionWCFpriming between Open2 and the first successful string-handle call (capture-tier; the highest-leverage single RE task for M1). - 2023 R2 gRPC server — these ops are first-class on the gRPC front door, where the handle/envelope differs and the registration wall may not apply.
Do not ship any string-handle op via guessed calls (project discipline: "leave them throwing until evidence supports an implementation").
⚠️ Update (2026-06-20): GETRP punches through — the wall is not absolute
Roadmap R1.2 GetRuntimeParameterAsync turned out to be a string-handle op
(aa/Stat/GETRP(string handle, byte[] pRequestBuff) → (bool, byte[] pResponseBuff, byte[] errorBuffer)) — the same shape as GETHI, and in the same native session it
uses the same handle GUID as GETHI (confirmed: the GUID equals the Open2 outBuff
storage-session id at [5..21], the value the managed ParseOpenConnectionResponse
already extracts as StorageSessionId).
Yet GETRP works from the pure-managed client — live-verified, returns the runtime
HistorianVersion value 20,0,000,000. The only material difference from the failed
GETHI probe is the handle string format: the native client sends the GUID
UPPERCASE, dash-separated, no braces (format example
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX, all hex upper), i.e.
storageSessionId.ToString("D").ToUpperInvariant(). .NET Guid.ToString("D") is
lowercase, so a probe that passed the GUID without upcasing would not byte-match what
the server's session table is keyed on.
Implication / open lead (not yet retested): the GETHI/ExeC/QTB/QTG family failures
may be (at least partly) a handle-format issue, not (only) a missing native
registration step. The highest-value cheap follow-up is to re-probe GETHI and ExeC
with the uppercased storage-session GUID before assuming the registration wall. If
they also return data, the "wall" collapses to a formatting bug and R1.4/R1.5/R1.6/R1.1
may be reachable without any new RE. This has not been done yet — do not reclassify
those items until it is. GETRP is shipped because it was directly captured + live-verified
end-to-end; the rest remain ProtocolEvidenceMissingException/unprobed until tested.
See HistorianRuntimeParameterProtocol, IStatusServiceContract2.GetRuntimeParameter,
golden WcfRuntimeParameterProtocolTests, and capture tooling
scripts/Capture-RuntimeParam.ps1 + scripts/decode-runtime-param-capture.py.