Record 2023 R2 binary-dive verdicts; fix revision-probe scope comment

Mined the full decompiled stock 2023 R2 managed client as the oracle for every
still-pending gRPC item. Governing fact: ArchestrA.HistorianAccess is a C++/CLI
shim into native HistorianClient; the managed Grpc*Client wrappers have zero
call sites, so buffer-building/dispatch for the pending items is native (absent
from the binaries). Sharpened verdicts into handoff.md:

- Items 4/5/6 + OpenStorageConnection: hard-confirmed walled, with real reasons
  (SQL gated out client-side via IsManagedHistorian; no Revision RPC in the gRPC
  contract; LoadBlocks response is a native blob behind the storage console handle).
- Items 3 (SendEvent) and 7 (DeleteTEP): moved from vague to precise, LOCAL-box
  capturable targets (PackToVtq btValues / DeleteTagExtendedPropertiesByName
  BtInput with deleteFromServer=true).

Also correct the HistorianGrpcRevisionProbe doc comment: it probes the
non-streamed ORIGINAL-insert path (AddNonStreamValues), a distinct capability
from revision EDITS (native AddRevisionValues trio, no gRPC RPC) — the prior
comment conflated them.

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-23 15:13:11 -04:00
parent cac81c7e5c
commit ae536bb4b8
2 changed files with 80 additions and 0 deletions
+68
View File
@@ -101,6 +101,74 @@ Live-server gRPC probe recipe: set
quotes — `reference_wonder_sql_vd03_credentials`) and run the gated
`HistorianGrpcIntegrationTests`.
### 2023 R2 stock-client binary dive (2026-06-23) — sharpened verdicts
Re-read the full decompiled stock 2023 R2 managed client
(`histsdk-2023r2-analysis/decompiled/`: `Archestra.Historian.GrpcClient`,
`ArchestrA.HistorianAccess`, `Archestra.Grpc.Contract`, `HistorianEvent`,
`HistorianAccessUtil`) as the oracle for every still-pending item. **Governing
fact:** `ArchestrA.HistorianAccess.dll` is a C++/CLI mixed assembly — every
data/config/write method is a thin shim into native `<Module>.HistorianClient.*`,
and the managed `Grpc*Client` wrappers are instantiated by **nothing** in the
decompiled set (`new Grpc*Client(` → zero call sites). So the buffer-building and
RPC-dispatch sequencing for these items lives in native C++ not present in the
binaries. That confirms the "gated" calls were not from missing managed steps —
with these refinements:
- **Item 1 (gRPC event rows)** — **confirmed native/server-side.** Stock event
call graph is provably identical to ours (transport, per-service channels,
gzip-only metadata, CM_EVENT registration, v8 ECDH Event-open, `StartEventQuery`
request bytes). `EventQuery.StartQuery`/`MoveNext` dispatch straight into native
`HistorianClient.StartEventQuery`/`GetNextRow`; the query orchestration that
would differ is native and not on the wire. One untested low-effort check
remains: byte-diff a captured **Event-connection** EnsureTags/RegisterTags
against our replay (the 83-vs-86-byte EnsT gap was never actually compared).
- **Item 3 (SendEvent over gRPC)** — **sharpened from "maybe no RPC" to a precise
capture.** RPC **confirmed** = `HistoryService.AddStreamValues` (the "no distinct
RPC" note is TRUE; an event rides the same RPC as a streamed sample, discriminated
inside `btValues`). Public API `HistorianAccess.AddStreamedValue(HistorianEvent)`
→ native `AddHistorianValue`; prereqs known (write-enabled Event conn, CM_EVENT
tag handle, quality 192); field set/order recovered from `HistorianEvent.PackToVtq`.
**Only the `btValues` VTQ byte layout is missing** — built by native
`CCommonArchestraEventValue::PackToVtq` and copied out as an opaque `CDataChunk`.
Our read parser already decodes the inverse property-bag format. **Capturable
against the local Historian** (instrument `PackToVtq` output / the `AddStreamValues`
body) → then build `HistorianEventWriteProtocol` and reuse the
`HistorianGrpcHistoricalWriteOrchestrator` plumbing.
- **Item 4 (ExecuteSql over gRPC)** — **confirmed walled + explained.** The stock
client gates SQL **out client-side**: `HistorianAccess.ExecuteSqlCommand` returns
`OperationNotSupported` when `IsManagedHistorian(node)` or `!IsProcessConnectionRequested()`
(decompile ~:6198/:6214) and never sends the RPC. SQL-over-gRPC is unsupported by
design on a managed/gRPC historian; our `ProtocolEvidenceMissingException` is correct.
- **Item 5 (R4.2 revision edits)** — **confirmed HARD.** There is **no Revision RPC
in the gRPC contract** (zero "Revision" message types); the stock client reaches a
revision edit only via the native `HistorianClient.AddRevisionValuesBegin/AddRevisionValue/
AddRevisionValuesEnd` transaction trio over the storage-engine channel. NOTE: this is
a **distinct capability** from `AddNonStreamValues` (non-streamed original insert) —
`HistorianGrpcRevisionProbe` probes the latter; its doc comment was corrected to say so.
- **Item 6 (ReadBlocks/LoadBlocks)** — `LoadBlocks` request is a trivial
handle+sequence cursor but the `historyBlocks` response is a native blob with no
managed decoder, and it needs the D2-blocked `OpenStorageConnection` console handle.
Walled.
- **Item 7 (DeleteTagExtendedProperties)** — **reframed to a capturable lead.** RPC +
string handle are **correct** in our SDK; ADD and DELETE are structurally identical
and **neither** routes through `StartJob`. The differentiator is the `deleteFromServer`
flag carried inside the native-built `BtInput` plus the native HCAL **cache-sync
background worker** that actually propagates the delete server-side (config writes hit
the in-process cache first, then sync). **Capturable**: capture native
`DeleteTagExtendedPropertiesByName(deleteFromServer=true)`'s `BtInput` to learn whether
one well-formed RPC durably deletes (→ shippable) or whether it genuinely depends on
the cache-sync worker (→ walled).
- **SF/snapshot/shard/ForwardSnapshot ops** — only `Get/SetSFParameter` are managed-built
(typed strings); all others carry opaque native buffers and need the storage console
handle. Walled / tooling-internal.
**Net:** 3 items hard-confirmed walled with real explanations (4, 5, 6 + OpenStorageConnection),
and **2 moved to a precise, local-box-capturable target**: **SendEvent** (`PackToVtq` output)
and **DeleteTEP** (`BtInput` with `deleteFromServer=true`). Both need native instrumentation of
`aahClientManaged.dll` (Frida / IL-rewrite — repo tooling exists under
`tools/AVEVA.Historian.NativeTraceHarness` + `scripts/frida/`), not a special server.
## Project Direction
The project goal is still a fully managed .NET 10 C# AVEVA Historian client.
@@ -20,6 +20,18 @@ namespace AVEVA.Historian.Client.Grpc;
/// write-enabled session and calls <c>AddNonStreamValuesBegin</c>, surfacing whatever the server
/// returns. It writes NO data — if Begin succeeds it immediately calls <c>AddNonStreamValuesEnd</c>
/// with <c>bCommit=false</c> to discard the transaction.
///
/// <para><b>Scope note (corrected 2026-06-23 after a 2023 R2 binary re-read).</b> Despite the type
/// name, this probes the <i>non-streamed ORIGINAL / backfill insert</i> capability
/// (<c>AddNonStreamValues</c>), which is a <b>distinct capability from a revision EDIT</b>
/// (overwriting an existing historized value with a new revision). The stock high-level client
/// reaches a revision edit via a separate native transaction trio
/// <c>HistorianClient.AddRevisionValuesBegin/AddRevisionValue/AddRevisionValuesEnd</c>
/// (<c>ArchestrA.HistorianAccess.AddRevisionValues</c>, REVISION_MODE ∈ InsertLatest/UpdateSingle/
/// UpdateMultiple). That trio has <b>NO corresponding RPC in the gRPC contract</b> (no "Revision"
/// message type exists in <c>Archestra.Grpc.Contract</c>) — it rides the native storage-engine
/// transaction channel only. So R4.2 revision edits remain unreachable over gRPC regardless of this
/// probe's outcome; this probe neither covers nor unblocks them.</para>
/// </summary>
internal sealed class HistorianGrpcRevisionProbe
{