R1.5 GetTagExtendedPropertiesAsync (GetTepByNm) + R1.6 closed (no op)

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
This commit is contained in:
Joseph Doherty
2026-06-20 22:52:07 -04:00
parent 4da5287d01
commit 108220c36b
13 changed files with 897 additions and 8 deletions
@@ -17,8 +17,18 @@
> `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. R1.5/R1.6 (GetTepByNm family) and QTB/QTG are very likely reachable the
> same way but have not yet been individually re-probed.
> was the uppercase format.
>
> **Update 2026-06-20 — R1.5 `GetTepByNm` shipped; QTB nuance.** `GetTagExtendedPropertiesFromName`
> (`GetTepByNm`) is now **shipped + live-verified** with the uppercase handle
> (`GetTagExtendedPropertiesAsync`; see `wcf-tag-extended-properties.md`). It confirms the
> string-handle Retrieval family is reachable (and `GetTgByNm`/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
> with `error 1` *server-side* (`CMdServer::StartTagQuery::StartActiveTagnamesQuery` over
> `\\.\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.
---
@@ -0,0 +1,104 @@
# Tag extended properties over 2020 WCF — GetTepByNm (HCAL R1.5)
**Status: ✅ DONE + live-verified (2026-06-20).** `HistorianClient.GetTagExtendedPropertiesAsync(tag)`
reads a tag's extended (user-defined) properties over the 2020 WCF op
`aa/Retr/GetTagExtendedPropertiesFromName` (`GetTepByNm`). Live-verified end-to-end from the
pure-managed .NET 10 client against the local 2020 Historian.
## The op
`GetTepByNm` is on the Retrieval service (`IRetrievalServiceContract4`):
```
bool GetTagExtendedPropertiesFromName(
string handle, // Open2 storage-session GUID, UPPERCASE dash-no-braces
byte[] tagNames, // [MessageParameter pRequestBuff-style]
ref uint sequence, // paging cursor (0 on first call)
out byte[] tagExtendedProperties, // result buffer
out byte[] errorBuffer)
```
It is a **string-handle** op — reachable from the managed client because the handle is the Open2
storage-session GUID formatted `storageSessionId.ToString("D").ToUpperInvariant()` (the same handle
format that unlocked GETRP/GETHI/ExeC; see `wcf-string-handle-wall.md`). The Retrieval service
version handshake (`Retr.GetV`) is primed first, as the native client does.
## Why the name-based path (not the TagQuery path)
There are two managed entry points:
- **Index-based** `TagQuery.GetTagExtendedPropertyInfo(start, count, …)` — requires a prior
`StartTagQuery` (`QTB`). On this 2020 box **QTB fails server-side** (`error 1` from
`CMdServer::StartTagQuery::StartActiveTagnamesQuery` over `\\.\pipe\aahMetadataServer\console`),
so this path is dead here regardless of handle format.
- **Name-based** `HistorianAccess.GetTagExtendedPropertiesByName(tagName, fetchFromServer, …)`
issues `GetTepByNm` directly with the tag name in `tagNames`, no QTB needed. Its second arg
forces a **server fetch** when true; when false the C++ client reads a local cache and returns
`error 41 (Requested item not found)` without any WCF round-trip. The SDK reproduces the
name-based path.
## Wire format (captured)
`scripts/Capture-TagExtendedProperties.ps1` (NativeTraceHarness `tag-extended-properties` scenario +
instrument-wcf-{write,read}message) → decode with `scripts/decode-tag-properties-capture.py`.
Golden-pinned in `WcfTagExtendedPropertyProtocolTests`.
### Request — `tagNames` buffer
```
uint32 count
per name: uint32 charCount + UTF-16LE chars
```
(For one tag: `01 00 00 00` + `LL 00 00 00` + UTF-16 name.)
### Response — `tagExtendedProperties` buffer
```
uint32 tagCount
per tag:
byte groupMarker (observed 0x01)
0x09 + uint16 byteLen + ASCII tagName (compact-ASCII string)
uint32 propertyCount
per property:
byte propMarker (observed 0x02 — likely the value type)
0x09 + uint16 byteLen + ASCII propertyName
0x43 + uint16 payloadLen + uint16 charCount + UTF-16LE value (VT_BSTR CRetVariant)
byte trailingMarker (observed 0x01)
```
`payloadLen` counts the `charCount` field (2 bytes) + the UTF-16 value bytes. Only the string value
variant (`0x43`) is evidence-backed; other variant types throw `ProtocolEvidenceMissingException`
(same discipline as GETRP). The single-byte `0x01`/`0x02`/`0x01` markers are pinned as observed
constants from a single capture; their full semantics are not independently disambiguated.
Example (sanitized — real capture used a dev tag/value):
```
tag "Reactor.Temp1" → property "Location" = "Plant/AreaA"
```
### Paging
`GetTepByNm` is sequence-paged like `GetNextQueryResultBuffer`: call with `sequence = 0`, parse the
buffer, then re-call with the returned `sequence`. A small result returns everything on the first
call; the next call returns an empty/`nil` buffer (with a benign `CClientUtil::FillBufferFromVector`
terminator) — that is the stop signal. The SDK loops until the buffer carries no rows.
## R1.6 (localized properties) — no distinct op on 2020
There is **no** `GetTagLocalizedPropertiesFromName` / `GetTlpByNm` op or
`GetTagLocalizedPropertiesByName` method in `current/aahClientManaged.dll` — the only "localized"
surfaces are `ClientApp.GetLocalizedText` and `SMessageTextMap.GetLocalizedMessage` (error-message /
UI-text localization), not tag properties. So R1.6 **collapses into R1.5**: extended properties
(`GetTepByNm`) are the user-defined tag-property read surface on 2020. R1.6 is closed as
"no separate op," not left throwing.
## Shipped surface
- `HistorianClient.GetTagExtendedPropertiesAsync(tag)``IReadOnlyList<HistorianTagExtendedProperty>`
(`Name`/`Value` pairs; empty when the tag has none).
- `HistorianTagExtendedPropertyProtocol` (serializer/parser), `HistorianWcfTagExtendedPropertyClient`
(orchestration), golden `WcfTagExtendedPropertyProtocolTests`, gated live
`GetTagExtendedPropertiesAsync_AgainstLocalHistorian_ReturnsProperties` (set `HISTORIAN_TEP_TAG`
to a tag with extended properties).