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
+9 -5
View File
@@ -98,17 +98,21 @@ read/browse/status surface is Windows-free and the gRPC stack is the default pat
> (and R1.1) take a **`string` GUID handle**; the earlier "code 1/51 blocked" verdict came from
> passing the Open2 storage GUID in .NET's default **lowercase**. Sent **uppercase**
> (`storageSessionId.ToString("D").ToUpperInvariant()`) the same handle works: **GETRP** (R1.2,
> shipped), **GETHI** (R1.4) and **ExeC** (R1.1) are all live-verified reachable. R1.5/R1.6
> (GetTepByNm family) + QTB/QTG are very likely reachable the same way (not yet individually
> re-probed). Full analysis: `docs/reverse-engineering/wcf-string-handle-wall.md` (RESOLVED banner).
> shipped), **GETHI** (R1.4) and **ExeC** (R1.1) are all live-verified reachable, and **R1.5
> `GetTepByNm`** is now **shipped + live-verified** (`GetTagExtendedPropertiesAsync`). **R1.6 has no
> distinct op** (collapses into R1.5). Note: `QTB` (StartTagQuery) does **not** punch through — it
> fails *server-side* (`CMdServer::StartActiveTagnamesQuery` over the `aahMetadataServer` pipe),
> independent of handle format, so the index-based property/query paths stay blocked here. Full
> analysis: `docs/reverse-engineering/wcf-string-handle-wall.md` (RESOLVED banner) and
> `docs/reverse-engineering/wcf-tag-extended-properties.md`.
> R1.8/R1.9 (StartQuery summary/state modes) are `uint`-handle and were already reachable.
### 1b. Bounded (decode one `bytes` payload; SM each)
| ID | Capability | gRPC op | Payload to decode | Depends |
|---|---|---|---|---|
| R1.4 | `GetHistorianInfoAsync` | `Status.GetHistorianInfo` (`GETHI`) | ✅ **REACHABLE (2026-06-20, live-probed)** via the uppercase storage GUID — `GETHI` returns data (`StringHandleProbeDiagnosticTests`). The version-keyed request returns `uint charCount + UTF-16`; the full info struct (incl. `EventStorageMode`@514) needs its own request capture. **Public API not yet shipped.** | uppercase string handle |
| R1.5 | Extended-property **read** | `Retrieval.GetTagExtendedPropertiesFromName` | 🟡 **Likely reachable** via uppercase string handle (GetTepByNm family) — not yet individually re-probed. TEP result buffer. | uppercase string handle |
| R1.6 | Localized-property **read** | `Retrieval.GetTagLocalizedPropertiesFromName` | 🟡 **Likely reachable** (same family) — not yet re-probed. | uppercase string handle |
| ~~R1.5~~ | Extended-property **read** | `Retrieval.GetTagExtendedPropertiesFromName` (`GetTepByNm`) | **DONE (2026-06-20), live-verified.** `GetTagExtendedPropertiesAsync(tag)` → name/value pairs. String-handle op via the uppercase storage GUID; name-based path (`GetTagExtendedPropertiesByName`, not the QTB-gated TagQuery path). 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. Shipped: `HistorianTagExtendedPropertyProtocol`, golden `WcfTagExtendedPropertyProtocolTests`, gated live test. See `docs/reverse-engineering/wcf-tag-extended-properties.md`. | uppercase string handle |
| ~~R1.6~~ | Localized-property **read** | (no op) | ⛔ **No distinct op on 2020 — collapses into R1.5.** There is no `GetTagLocalizedPropertiesFromName`/`GetTlpByNm` or `GetTagLocalizedPropertiesByName` in `current/aahClientManaged.dll`; the only "localized" surfaces are error-message/UI-text localization. Extended properties (R1.5) are the user-defined tag-property read surface. Closed, not throwing. | — |
| ~~R1.7~~ | Event **filters** | filter bytes in `Retrieval.StartEventQuery` | ✅ **DONE (2026-06-20), live-honored.** `ReadEventsAsync(start, end, HistorianEventFilter)`. The filter rides `StartEventQuery`'s `pRequestBuff` (captured via `EventQuery.AddEventFilter` + instrument-wcf-writemessage; Equal vs Contains diffed to isolate the op). Filter block: `ushort 0 + uint filterCount + uint condCount + uint nameLen + name(UTF-16) + uint 1 + ushort op + uint 1 + value(0x09-len-0x00 compact-ASCII) + byte 0`. **REAL, not inert** (a non-matching predicate returns 0 events; matching returns the subset). Single string-valued predicate only; multi-filter (OR) / multi-condition (AND via `AddEventFilterCondition`) framing not yet fully captured. See `HistorianEventFilter`, golden `WcfEventQueryProtocolTests`. | — |
| R1.8 | Analog-summary query | `Retrieval.StartQuery` (summary mode) | summary row layout — **`uint`-handle, reachable. Scoped + decode targets located** (`CAnalogSummaryValue.UnpackFromValueBuffer`, fields Min/Max/First/Last/ValueCount/Integral/…). Plan: [`r1.8-r1.9-summary-queries.md`](r1.8-r1.9-summary-queries.md) | — |
| R1.9 | State-summary query | `Retrieval.StartQuery` (state mode) | state-summary row layout — **`uint`-handle, reachable. Scoped** (`CStateSummaryStruct`: MinContained/MaxContained/TotalContained/PartialStart/PartialEnd/StateEntryCount). Plan: [`r1.8-r1.9-summary-queries.md`](r1.8-r1.9-summary-queries.md) | — |
@@ -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).