M3 R3.1: OpenStorageConnection is a dead end (error 85); precondition is front-door RegisterTags

Live-probed StorageService.OpenStorageConnection against the 2023 R2 server over a
write-enabled (0x401) session. Every attempt — sweeping ConnectionMode (0x401/0x402/0x1),
StorageSessionId-in (Open2-GUID / empty), and FreeDiskSpace — returns the IDENTICAL native
error type=4 code=85 ("session not registered"), so it's a structural refusal, not a bad
field value.

Decode (two corroborating facts):
- Error 85 is the same code the event read returns before RegisterTags2 (see
  HistorianWcfEventOrchestrator) — a generic "session not registered for this op".
- The 2023 R2 decompile shows OpenStorageConnection lives on a SEPARATE GrpcStorageClient
  (the storage engine's SF/snapshot channel, own port + service identity); HistorianAccess
  drives non-streamed writes through the native C++ HistorianClient, never this op.

So the roadmap's mapped "missing console session" step was wrong. The real non-streamed-write
precondition is the front-door HistoryService.RegisterTags (RTag2-family) for the target tag —
which is exactly why the R3.1 batch failed at AddNonStreamValues (no tag registered ->
StoreNonStreamValues had no route). Matches the original 2020-WCF D2 hypothesis.

Remaining (both need a native gRPC capture; do not guess bytes): the regular-tag RegisterTags
btTagInfos (only CM_EVENT's tag-GUID form is known) and the AddNonStreamValues btInput.

- HistorianGrpcStorageConnectionProbe + grpc-open-storage-connection CLI (opens nothing
  persistent; CloseStorageConnection on success)
- corrected revision-write-path.md §R3.1 follow-up + hcal-roadmap R3.1/R3.2 rows
- gated regression test pinning the error-85 refusal

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-21 18:51:16 -04:00
parent 78cb689bdf
commit 57b9506d01
5 changed files with 345 additions and 5 deletions
@@ -76,6 +76,7 @@ try
"capture-tag-info" => CaptureTagInfo(args),
"grpc-revision-probe" => ProbeGrpcRevision(args),
"grpc-nonstream-decode" => ProbeGrpcNonStreamedDecode(args),
"grpc-open-storage-connection" => ProbeGrpcOpenStorageConnection(args),
_ => UnknownCommand(args[0])
};
}
@@ -3213,6 +3214,43 @@ static int WriteMarker(string[] args)
return 0;
}
static int ProbeGrpcOpenStorageConnection(string[] args)
{
// Usage: grpc-open-storage-connection <host> [port] [--tls] [--dnsid <name>]
// M3 follow-up step 1: probe StorageService.OpenStorageConnection — the missing storage-engine
// console-session precondition the R3.1 decode pinned. No btInput to guess (all 12 fields are
// typed); the probe sweeps the uncertain VALUES and reports the server response per attempt.
// Opens NOTHING persistent — on success it CloseStorageConnections immediately.
string host = args.Length > 1 ? args[1] : "localhost";
int port = args.Length > 2 && int.TryParse(args[2], out int parsedPort)
? parsedPort
: HistorianClientOptions.DefaultGrpcPort;
bool tls = HasOption(args, "--tls");
string? dnsId = GetOption(args, "--dnsid");
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
bool explicitCreds = !string.IsNullOrEmpty(user);
var options = new HistorianClientOptions
{
Host = host,
Port = port,
Transport = HistorianTransport.RemoteGrpc,
GrpcUseTls = tls,
AllowUntrustedServerCertificate = tls,
ServerDnsIdentity = dnsId,
IntegratedSecurity = !explicitCreds,
UserName = user ?? string.Empty,
Password = password ?? string.Empty,
};
var probe = new HistorianGrpcStorageConnectionProbe(options);
HistorianGrpcOpenStorageConnectionResult result = probe.ProbeAsync(CancellationToken.None).GetAwaiter().GetResult();
Console.WriteLine(JsonSerializer.Serialize(result, CreateJsonOptions()));
return result.OpenStorageSucceeded ? 0 : 2;
}
static int ProbeGrpcRevision(string[] args)
{
// Usage: grpc-revision-probe <host> [port] [--tls] [--dnsid <name>] [--insecure-cert]