Files
histsdk/docs/plans/write-commands-reverse-engineering.md
T
Joseph Doherty 5ce62a5900 Wire ApplyScaling, StorageRate; close out write-commands plan
ApplyScaling (HistorianTagDefinition.ApplyScaling):
The EnsT2 trailer's second byte controls server-side scaling — `FE 00`
mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists
distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling
set_ApplyScaling on the native harness and capturing the wire bytes for
both values with identical inputs. The earlier docs claimed
EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has
no such operation — toggling that one byte is the whole fix.

StorageRate (HistorianTagDefinition.StorageRateMs):
Serializer accepts a non-default rate, validated empirically against
the live server which only accepts quantized values
(1000/5000/10000/60000/300000 ms).

EnsureTagAsync upsert semantics:
Second call on the same tag name with different fields succeeds and
updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place
(verified by direct SQL inspection in a live test).

Plan + doc closeout:
write-commands-reverse-engineering.md rewritten as a current-state
plan with three workstreams (A doc closeout / B idempotency / C1
StorageRate) and a parallelism table; prior phase notes preserved as
appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md,
README.md updated to remove "writes are out of scope" / non-existent
UpdateTags references and document the actual EnsT2 wire format
including the `FE xx` trailer.

Reverse-engineering harness gains --write-apply-scaling and a SQL
post-check that prints the persisted AnalogTag bounds so future RE
sessions can verify wire→DB causality without leaving the harness.

169/169 tests pass (was 165; +4 new tests covering ApplyScaling,
StorageRate golden bytes, StorageRate live persistence, and
EnsureTagAsync upsert semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:04:27 -04:00

11 KiB
Raw Blame History

Plan: Reverse-Engineering Write Commands

Status (2026-05-04, post-ApplyScaling landing)

Phase 2 is complete. The write surface that the server actually supports for managed clients is implemented and live-verified:

  • EnsureTagAsync for analog tags (Float, Double, Int2, Int4, UInt4), with optional ApplyScaling=true for distinct MinRaw/MaxRaw persistence (AnalogTag.Scaling=1).
  • DeleteTagAsync for any tag created via the SDK.

Architecturally blocked / out of scope:

  • AddS2 (write samples) — server's runtime cache only ingests from configured IOServers / Application Server pipelines, not from client-only AddTag flows. The native wrapper hits the same wall; this is a server architecture decision, not a protocol gap.
  • Discrete / String / Int1 / Int8 / UInt8 tag types — fail at native HistorianAccess.AddTag before any wire bytes leave the client. Likely require a different code path (AddTagExtendedProperties or pre-population via SMC); not investigated.

This plan covers the residual workstreams.

Workstreams

A. Documentation closeout

Status docs across the repo still describe write commands as "in progress" or speculate about a non-existent UpdateTags operation. Closing those out so future agents don't re-walk the same dead ends.

Step File Action
A1 docs/reverse-engineering/handoff.md Add a "Write-flow status" note marking EnsT2/DelT live; remove stale write-blocker callouts
A2 docs/reverse-engineering/implementation-status.md Flip EnsT2 / DelT rows from "out of scope" to "implemented"; add ApplyScaling row
A3 docs/reverse-engineering/wcf-contract-evidence.md Add evidence rows for EnsT2(analog) and DelT pointing at the captured fixtures
A4 README.md Operation-status table reflects the two write ops

B. EnsT2 idempotency / update behavior

We don't currently know what happens when EnsureTagAsync is called against a tag name that already exists with different fields. Three plausible outcomes: server errors, server silently updates, server no-ops. This affects how callers should think about the API (create-only vs upsert).

Step Action
B1 Add a live integration test that calls EnsureTagAsync twice on the same tag name with different MinEU/MaxEU/Description; query SQL after each call to capture observed behavior
B2 Document the observed contract in HistorianTagDefinition doc-comment and (if surprising) in CLAUDE.md

C. Expose currently-hardcoded CTagMetadata fields

The serializer hardcodes StorageRate=1000ms, StorageType=Cyclic, IntegralDivisor=1.0, and a few flag-block bytes. The server accepts those defaults so existing tests pass, but customers building tags with non-default rates can't currently express that.

Step Field Effort Notes
C1 StorageRate (uint32 ms) small — wire field is already at a known offset, just plumb a parameter through Default stays 1000ms
C2 StorageType (Cyclic / Delta) medium — need a comparison capture to find which byte in the flag block encodes it Deferred unless customer asks
C3 IntegralDivisor (double) small — wire field already known Deferred unless customer asks

C1 is the only one I'm executing in this round. C2/C3 are listed for completeness; pick them up when there's a concrete request.

D. Deferred — no current evidence or customer ask

ID Item Why deferred
D1 AddTagExtendedProperties / DeleteTagExtendedProperties No wire captures yet; no customer ask
D2 AddRevisionValuesBegin/Value/End (revision-write path) Multi-step capture needed against an existing historized tag; complex; no customer ask
D3 Discrete/String/Int1/Int8/UInt8 EnsT2 root cause Native AddTag fails for these — likely requires an entirely different code path; would need a fresh capture and IL walkthrough

Parallelism

Track Files touched Conflicts with
A1 docs/reverse-engineering/handoff.md none
A2 docs/reverse-engineering/implementation-status.md none
A3 docs/reverse-engineering/wcf-contract-evidence.md none
A4 README.md none
B tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs, src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs C1 (same HistorianTagDefinition file)
C1 src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs, src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs, src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs, tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs, tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs B (same HistorianTagDefinition and integration-test file)

Concurrency-safe groupings:

  • A1A4 are pairwise independent and pairwise independent of B and C1 → can be assigned to four agents in parallel.
  • B and C1 both touch HistorianTagDefinition.cs. Sequence them: B first (it just adds a doc-comment) then C1 (which adds a field).

In a single-agent execution (this session), the order is: A* batched edits → B → C1 → build/test → commit.

Success Criteria

  • A1A4: documentation reflects the actual current write surface and removes references to non-existent operations (UpdateTags).
  • B: a passing live test that asserts the observed double-EnsT2 behavior, plus a doc-comment update on HistorianTagDefinition.
  • C1: HistorianTagDefinition.StorageRateMs field exposed, default preserves existing wire output, golden test for a non-default rate, live test that creates a tag with a non-default rate and asserts via SQL that Tag.CurrentEditorUserKey (or the storage-rate column, TBD) reflects the value.
  • All 165+ tests still pass; no regression in existing live tests.

Appendix: Prior Phase Notes

The historical phase logs that drove the implementation are preserved below for context. They describe the path from "write surface unknown" to "write surface implemented and live-verified" as it unfolded through 2026-05-04. Anything in this appendix that contradicts the current Status section above is superseded.

Phase 1 findings (recorded, not implementing)

§3.4 ModifyData/DeleteData — ELIMINATED FROM SCOPE

methods aahClientManaged.dll returns no managed wrapper for any of: EditValue, ModifyValue, EditData, DeleteData, ModifyData, OverwriteData. Per the plan's §3.4 disposition rule, this op is REST-only / SMC-only and remains out of scope for the SDK.

§4.a Native serializers identified

The wrapper does have managed-public write API:

Public method Token Used for
ArchestrA.HistorianAccess.AddTag 0x0600619A Creates a new tag (drives EnsT2)
ArchestrA.HistorianAccess.AddStreamedValue 0x0600618C/D/E (3 overloads) Pushes one timestamped value (drives AddS2)
ArchestrA.HistorianAccess.AddNonStreamedValue 0x0600618F/90 (2 overloads) Pushes one timestamped value, non-stream mode
ArchestrA.HistorianAccess.DeleteTags 0x060061A4 Removes tags (drives DelT)
ArchestrA.HistorianAccess.AddVersionedStreamedValue 0x0600616F Pushes versioned value (rev edit)
ArchestrA.HistorianAccess.AddRevisionValuesBegin/Value/End/AddRevisionValues 0x06006175-77, 0x0600617F Multi-row revision write

Native serializer for EnsT2: <Module>.CTagUtil.ConvertTagMetadataToHistorianTag at token 0x060055CE. WCF wrapper for AddS2: <Module>.CHistoryConnectionWCF.AddStreamValuesToHistorian at token 0x0600404C.

Phase 2 follow-on findings (2026-05-04, second pass)

The AddS2 prereq is architectural, not protocol-level. Three follow-up attempts to trigger AddS2 from the sandbox harness all hit a client-side gate before any AddS2 byte reaches the wire:

  1. TagKey synthetic→real override: even with the real wwTagKey the server returns error 129 "Tag not found in cache".
  2. Server-cache settle wait of 8s: error 129 persists.
  3. Fresh process / fresh connection (skip AddTag): error 129; no AddS2 bytes sent on wire.

The Historian engine's runtime tag cache only ingests tags from configured IOServers / Application Server pipelines, not from HistorianAccess.AddTag-only flows. WriteValueAsync cannot be implemented as a generic client API against this server architecture.

Phase 2 results (write captures + EnsureTagAsync/DeleteTagAsync)

EnsT2 + DelT priming chain captured (no RTag2 between Open2 and EnsT2):

Hist.GetV → Hist.GetI ×2 → Hist.ValCl ×2 → Hist.Open2 →
Stat.GetV ×2 → Stat.GETHI ×2 → Hist.UpdC3 →
Stat.GetSystemParameter ×7 → Trx.GetV → Stat.GetV → Retr.GetV →
Hist.EnsT2 → Hist.Close2

Open2 with NativeIntegratedWriteEnabledConnectionMode = 0x401 (Process | Write | IntegratedSecurity) is required — read-mode 0x402 makes the server return err 132 OperationNotEnabled silently. The analog Float CTagMetadata payload is 144 bytes with a leading 0x4E marker byte and a 2-byte trailer FE xx where the second byte is the ApplyScaling flag (00 = false, 01 = true).

ApplyScaling resolution (2026-05-04)

Earlier docs claimed "MinRaw is mirrored to MinEU — server quirk, not SDK bug". That conclusion was based on tests that always set ApplyScaling=false on the native side. Re-running with set_ApplyScaling(true) on the harness and capturing wire bytes for both values revealed:

  • ApplyScaling=false → trailer = FE 00 → server mirrors MinRaw→MinEU, sets AnalogTag.Scaling=0
  • ApplyScaling=true → trailer = FE 01 → server persists distinct MinRaw/MaxRaw, sets AnalogTag.Scaling=1

The IHistoryServiceContract2 surface has no UpdateTags operation. Distinct MinRaw/MaxRaw persistence is achieved entirely by toggling that one byte in the EnsT2 payload. The SDK now exposes this via HistorianTagDefinition.ApplyScaling.

Capture artifacts: artifacts/reverse-engineering/apply-scaling-experiment/enst2-applyscaling-{false,true}.ndjson.

Original goal section (preserved for historical reference)

"Write commands work" was originally defined as the four ops: EnsT2, AddS2, DelT, and ModifyData/DeleteData. The realized scope is EnsT2 + DelT only. AddS2 is permanently blocked by server architecture; ModifyData/DeleteData were eliminated by static analysis (no managed wrapper exists). The AddRevisionValuesBegin/Value/End chain remains a stretch goal (item D2 in the current plan) — it was never investigated because no SDK consumer has asked for revision writes.

Original safety rules (still applicable)

  • Single dedicated sandbox tag per RE session, name must start with RetestSdkWrite.
  • Never write to any tag named in HISTORIAN_TEST_TAG, HISTORIAN_TAG_FILTER, the docs, or the captured RE ndjson.
  • Time bounds on writes: every test uses DateTime.UtcNow so writes land inside the live RealTimeWindow / FutureTimeThreshold.
  • localhost only; no customer / corporate hosts.
  • Sanitization scan after every session.
  • Write captures live in artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/ (gitignored).