Compare commits

...

6 Commits

Author SHA1 Message Date
Joseph Doherty f2a422b02f docs(auth): canonical scope strings in the two remaining stale lists
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m18s
ci / java (push) Successful in 2m58s
ci / portable (push) Successful in 8m49s
Both lists predate the scope rename and would mislead anyone creating a key:
CLAUDE.md's Authentication section still named the pre-rename scopes, and
docs/Authentication.md's ops.alice example passed 'read,write', which
GatewayScopes.ValidateScopes rejects outright. Same defect family as the
Build/Test/Run sample fixed in a5f843c.

Recorded as a follow-up: code review finding Server-012 claims it fixed the two
CLAUDE.md lists on 2026-05-18, but neither correction was present — a Resolved
finding is not re-examined, so the sibling Server-0xx doc resolutions want a
spot-check for the same pattern.
2026-08-18 06:13:52 -04:00
Joseph Doherty a5f843cbfb chore(plan): close the record — prior follow-ups annotated, closeout as-built noted
The prior plan's "Follow-ups recorded, not started" block described pre-branch
behavior; every item is now closed, narrowed, or restated with its evidence, so
the block no longer misleads a reader who lands on it first. The stale Rust-guard
bullet is corrected in place rather than deleted: Check 3 always existed, and
saying so is the only way the reader learns what the real (one-directional) gap was.

Also fixes the CLAUDE.md apikey sample, which named a verb the parser has never
accepted ('create'; only 'create-key' exists, no alias), omitted the required
--key-id, and listed non-canonical scope strings that GatewayScopes now rejects
at create time — the sample could not have run.
2026-08-18 06:12:31 -04:00
Joseph Doherty c3c603f169 docs(alarms): describe the real StreamAlarms open sequence, and close TST-16
The final integration review's non-blocker reservations, all documentation
or comment truth except one test arm.

The alarm feed opens provider_status -> snapshot_status -> cached
active_alarm -> snapshot_complete, which is what GatewayAlarmMonitor has
done since the snapshot_status frame landed. Two places still described
the old order: docs/Grpc.md said provider_status arrived *after* the
initial snapshot, contradicting its own snapshot_status section two
paragraphs down, and AlarmFeedMessage's leading proto comment named
neither status frame at all. Both now state the sequence the monitor
emits, so a client author reading either one gets the frame order right.

The proto comment change flows through the generated trees (Contracts,
Go, Java) and the client descriptor set; the Rust vendored copy stays
byte-identical to canonical. Python's generator does not carry proto
comments into its output, so it has no delta.

AlarmsHubPublisherTests' valueless-payload case covered snapshot_complete
and provider_status but not snapshot_status, leaving the newest arm
unpinned against the redaction switch that must ignore it. Added.

WnWrapAlarmConsumer's ack comment led with the 2026-05-01 reading that
-55 tracks the 8-arg overload, then refuted itself six lines later with
the 2026-08-18 probe. It now leads with the observation labelled as
narrower than it reads -- mirroring the correction already in
docs/AlarmClientDiscovery.md -- so the block argues one thing: the 6-arg
call site stays for parity, and rc semantics are per the probe. A
paragraph orphaned by an earlier splice is rewrapped. Comment interior
only; the file compiles on Windows.

TST-16 gets a dated closure note rather than a rewrite: the flag it
called dead was implemented 2026-08-18. GatewayDashboardDesign's /browse
paragraph gains the failed-read carve-out GatewayConfiguration already
documented, so the two agree that a failed read keeps its - placeholder.
2026-08-18 06:09:06 -04:00
Joseph Doherty a95e510015 chore(plan): Task 8 verification-green at 90331b6 — residuals recorded 2026-08-18 05:56:45 -04:00
Joseph Doherty 47d74032e6 chore(plan): Task 7 complete — probe retry answered Q2 and the re-raise leg, ack leg re-blocked with evidence 2026-08-18 05:48:10 -04:00
Joseph Doherty 540a1188aa docs(alarms): stop describing the wnwrap ack as working, and say the re-raise replaces the record
Follow-up to 90331b6. Three comment/prose corrections, no behaviour change.

AcknowledgeByName's comment still said the 6-arg overload "works and reaches the
alarm-history path correctly", which the same commit's own findings contradict in
three other places. It now says what was observed: rc=0 means accepted, not
applied — the 2026-08-18 probe acked a live alarm six ways and the snapshot, the
OPERATOR_NAME field, and the extension's .Acked attribute all stayed put. The -55
tracks the consumer, not the overload. Subscribe's comment gets the same
treatment: "lets AlarmAckByName succeed" becomes "return rc=0".

AlarmProbeFindings.md said the re-raise arrives as "a separate record" alongside
the returned one, which reads as coexistence and is wrong. The snapshot carries
one record per tag: the cap=1024 replies bracketing the re-raise are both
elementCount=3 (one per TestMachine_00{1,2,3}) at an identical 1613 bytes, and the
old GUID is absent from the later one. The re-raise replaces the record, so a
single poll spanning it sees the Clear and the Raise together.

Worker diff verified strictly comment-only; builds x86 on windev, 0W/0E.
2026-08-18 05:46:35 -04:00
17 changed files with 205 additions and 61 deletions
+2 -2
View File
@@ -71,7 +71,7 @@ dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Test
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
# API-key admin CLI (same exe, "apikey" subcommand)
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,invoke,event,metadata,admin
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj -- apikey create-key --key-id dev --display-name "dev" --scopes session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read,admin
```
Single test by name (xUnit `--filter`):
@@ -155,7 +155,7 @@ External analysis sources referenced by design docs:
## Authentication
Gateway gRPC clients authenticate with an API key in metadata: `authorization: Bearer mxgw_<key-id>_<secret>`. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session`, `invoke`, `event`, `metadata`, `admin`) gate specific RPCs; missing → `Unauthenticated`, insufficient → `PermissionDenied`. The `apikey` subcommand on the server exe manages keys; see `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/`.
Gateway gRPC clients authenticate with an API key in metadata: `authorization: Bearer mxgw_<key-id>_<secret>`. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`) gate specific RPCs; missing → `Unauthenticated`, insufficient → `PermissionDenied`. The `apikey` subcommand on the server exe manages keys; see `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/`.
Session event streaming is **owner-scoped**: the API key that opened a session is recorded on the session, and every `StreamEvents` attach/reattach is rejected with `PermissionDenied` unless the caller's key id matches the owner. Possessing the `event` scope and knowing a session id is not sufficient — this closes the reconnect/fan-out trust boundary (detach-grace and replay retention are on by default) so an `event`-scoped key cannot attach to another key's retained session.
@@ -355,6 +355,8 @@ If TST-02's interim mitigation (flip retention off) is chosen instead of impleme
**Verification.** `dotnet build src/ZB.MOM.WW.MxGateway.Server`; test toggling the flag suppresses/shows values; docs match.
**Closed 2026-08-18 — implemented (the Design's recommended path).** The flag is no longer dead: it now gates value display on all three seams that carry a tag value. `Dashboard/DashboardLiveDataService.cs` substitutes `[redacted]` at the service boundary for each successfully read `/browse` tag (a failed read keeps its `-` placeholder, so the error row and the redaction cannot contradict each other); `Dashboard/Hubs/AlarmsHubPublisher.cs` clears `current_value`/`limit_value` from a **deep-cloned** `AlarmFeedMessage` before broadcasting to `/hubs/alarms`, and `DashboardEventBroadcaster` does the same for the events-hub mirror — clones, because the sources fan out to gRPC `StreamAlarms`/`StreamEvents` and the replay ring, so no gRPC client is affected by this dashboard-display flag. The flag is **kept**, default still `false`. The stale prose the Finding flagged is gone: `docs/GatewayConfiguration.md` and `docs/GatewayDashboardDesign.md` now describe the real behaviour. Status tracked in `00-tracking.md` (already **Done**).
---
## TST-17 — Vendor-gated alarm parity residuals silently lossy `Medium` · `—`
@@ -7342,9 +7342,13 @@ func (x *StreamAlarmsRequest) GetAlarmFilterPrefix() string {
return ""
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
type AlarmFeedMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
@@ -89203,9 +89203,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
}
/**
* <pre>
* One message on the StreamAlarms feed. The stream opens with one
* `active_alarm` per currently-active alarm, then a single
* `snapshot_complete`, then a `transition` for every subsequent change.
* One message on the StreamAlarms feed. The stream opens with a
* `provider_status` (the current provider mode), then a `snapshot_status`
* (whether the cached active set may be incomplete), then one `active_alarm`
* per currently-active alarm, then a single `snapshot_complete`. After that
* come the live frames: a `transition` for every subsequent alarm change,
* interleaved with a further `provider_status` on each failover/failback and a
* further `snapshot_status` on each change of the truncation verdict.
* </pre>
*
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
@@ -89739,9 +89743,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
}
/**
* <pre>
* One message on the StreamAlarms feed. The stream opens with one
* `active_alarm` per currently-active alarm, then a single
* `snapshot_complete`, then a `transition` for every subsequent change.
* One message on the StreamAlarms feed. The stream opens with a
* `provider_status` (the current provider mode), then a `snapshot_status`
* (whether the cached active set may be incomplete), then one `active_alarm`
* per currently-active alarm, then a single `snapshot_complete`. After that
* come the live frames: a `transition` for every subsequent alarm change,
* interleaved with a further `provider_status` on each failover/failback and a
* further `snapshot_status` on each change of the truncation verdict.
* </pre>
*
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
+7 -3
View File
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
string alarm_filter_prefix = 2;
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
message AlarmFeedMessage {
oneof payload {
// Part of the initial active-alarm snapshot (ConditionRefresh).
+8 -2
View File
@@ -186,8 +186,14 @@ after-reraise TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC
Two facts, one confirming and one new. The `ALM → RTN` leg holds its GUID, reconfirming the
2026-05-01 capture against a write-driven rather than script-driven transition. The re-raise
does not: the returned record keeps the old GUID and the new active condition arrives as a
separate record under a fresh one.
does not: the record comes back under a GUID that has never been seen before.
The two records do **not** coexist — the snapshot carries one record per tag, and the
re-raise replaces the returned record rather than joining it. Both fetches above are the
full `cap=1024` reply, byte-identical in length (1613) at `@COUNT=3`/`elementCount=3`, the
three records being one per `TestMachine_00{1,2,3}`; `72B84A6E…` is simply absent from the
later one. So a single poll spanning the re-raise sees the old GUID disappear and the new
one appear together.
That is the correct reading for `ComputeTransitions` rather than a problem for it. The old
GUID leaving the active set is a real Clear and the new GUID is a real Raise, because they
+1 -1
View File
@@ -260,7 +260,7 @@ Examples:
```bash
mxgateway apikey init-db
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes invoke:read,invoke:write
mxgateway apikey create-key --key-id area1.reader --display-name "Area 1 reader" --scopes invoke:read,metadata:read --read-subtree "Area1/*" --browse-subtree "Area1/*"
mxgateway apikey create-key --key-id ops.temp --display-name "Temp contractor" --scopes invoke:read --expires 90d
mxgateway apikey create-key --key-id team-a.svc --display-name "Team A service" --scopes session:open,invoke:read --dashboard-tags team-a
+3 -1
View File
@@ -520,7 +520,9 @@ panel. The panel shows each subscribed tag's live value, MXAccess data type,
quality and source timestamp, refreshed every two seconds — but the value column
obeys `Dashboard:ShowTagValues` like every other dashboard value surface. With
the flag false (the default) `DashboardLiveDataService` hands the page
`[redacted]` in place of the formatted value; data type, quality, source
`[redacted]` in place of the formatted value of each **successfully read** tag —
a failed read keeps its `-` placeholder, since there was no value to suppress —
while data type, quality, source
timestamp and any read error are untouched, so the panel still answers "is this
tag advising and healthy" without disclosing the value. The substitution happens
at the service, not in the page: one decision point, and the value never enters
+5 -4
View File
@@ -114,10 +114,11 @@ message AlarmProviderStatus {
}
```
The gateway emits `provider_status` once when a client first subscribes
(immediately after the initial snapshot and before the first live transition)
and again on every failover or failback. A late-joining client therefore
always learns the current provider mode without waiting for the next switch.
The gateway emits `provider_status` once when a client first subscribes — as
the **first** frame on the stream, before the `snapshot_status` frame and
before any cached `active_alarm` — and again on every failover or failback. A
late-joining client therefore learns the current provider mode before it sees a
single alarm, without waiting for the next switch.
#### Snapshot completeness on the alarm feed
+38 -16
View File
@@ -416,22 +416,44 @@ gateway 1123/1123, live MXAccess smoke 8/8 — green first try, no stale-obj qui
had diverged from Contracts (masked by the in-repo build path); refreshed byte-identical,
and the client protoset descriptors were regenerated.
Follow-ups recorded, not started:
- `IGatewayAlarmService.StreamAsync` / `AlarmFeedMessage` does not carry the truncation
signal — live central-feed consumers (lmxopcua, ScadaBridge) cannot see snapshot
degradation; add if those consumers need completeness reasoning.
- No guard keeps `clients/rust/protos/` in sync with Contracts (a `diff` check in
`scripts/check-codegen.ps1` would close it).
- `EffectiveDashboardConfiguration` (dashboard settings page) doesn't display
`GroupToTag` / `UntaggedSessionVisibility`, though it shows `GroupToRole`.
- ApiKeysPage's `ConstraintText` neither offers tag input nor lists `DashboardTags`,
and since `IsEmpty` now counts tags, a tags-only key renders `-` where a truly
unconstrained key renders `unconstrained` — two spellings of one meaning.
- `AlarmsHubPublisher` broadcasts alarm transitions with `current_value`/`limit_value`
unredacted — the `ShowTagValues` redaction covers only the events hub mirror
(pre-existing; now noted in `docs/GatewayConfiguration.md`).
- The alarm probes' remaining questions (ack-leg GUID stability, `@COUNT` semantics)
unblock via the paths in `docs/AlarmProbeFindings.md`.
Follow-ups recorded here — all closed or narrowed on 2026-08-18 (branch
`feat/followup-closeout`, plan `docs/plans/2026-08-17-followup-closeout.md`).
Each bullet states what it said when written, then its closure:
- `IGatewayAlarmService.StreamAsync` / `AlarmFeedMessage` did not carry the truncation
signal — live central-feed consumers (lmxopcua, ScadaBridge) could not see snapshot
degradation. **Closed 2026-08-18 — shipped.** `AlarmSnapshotStatus` with
`snapshot_status = 5` on the feed: gateway-side edge emission plus open-time priming
(`fccf753`, `312e072`), rolled out to all five clients (`c748361`, `4d4f443`).
- "No guard keeps `clients/rust/protos/` in sync with Contracts." **Was stale when
written.** `scripts/check-codegen.ps1` Check 3 already existed and already byte-compared
by SHA-256 in CI. The real gap was narrower — the sweep ran in one direction only, so a
canonical proto with no vendored copy passed. **Closed 2026-08-18** by `711793f`: Check 3
now sweeps both directions and a canonical proto missing its vendored copy fails.
- `EffectiveDashboardConfiguration` (dashboard settings page) didn't display
`GroupToTag` / `UntaggedSessionVisibility`, though it showed `GroupToRole`.
**Closed 2026-08-18 — shipped** (`094f2ff`; comparer propagation fixed in `ff16c69`).
- ApiKeysPage's `ConstraintText` neither offered tag input nor listed `DashboardTags`,
and since `IsEmpty` counts tags, a tags-only key rendered `-` where a truly
unconstrained key rendered `unconstrained`. **Closed 2026-08-18 — shipped** (`c037d99`),
with the create form de-duplicating typed tags ordinal-ignore-case to match the CLI
(`fab600d`).
- `AlarmsHubPublisher` broadcast alarm transitions with `current_value`/`limit_value`
unredacted — the `ShowTagValues` redaction covered only the events hub mirror.
**Closed 2026-08-18** together with the `/browse` residual: `ShowTagValues` now gates
all three seams (`eff17d1`, `1ea6f60`, `e50ec49`).
- The alarm probes' remaining questions (ack-leg GUID stability, `@COUNT` semantics).
**Narrowed 2026-08-18** by a second probe attempt that used the secured-write verb the
first attempt lacked (`90331b6`, `540a118`):
- `@COUNT` — **answered.** It reports records in the reply, not matching records in the
provider, so exact truncation detection through it is permanently refuted; the
heuristic in `IsTruncatedFetch` stays as the only available signal.
- clear-then-re-raise GUID — **answered.** The re-raise carries a new GUID that replaces
the record rather than coexisting with it, which makes the existing `Clear`+`Raise`
handling correct as written.
- ack leg — **still blocked**, now with a sharper cause: the ack returns `rc=0` and is
accepted but inert, so the GUID never moves. Remaining unblock paths are recorded in
`docs/AlarmProbeFindings.md`.
---
@@ -448,3 +448,65 @@ clear obj/bin and rebuild, not a regression. Record all counts.
payload-case row), closeout-plan follow-ups→Task 9 only.
- Task 7/8 run against windev over `ssh windev` (PowerShell); psbridge is fallback.
- Do not merge to `main` without user instruction.
---
## As-built notes (execution record, 2026-08-18)
All 9 tasks completed on `feat/followup-closeout`; every classification-driven review
chain resolved **Approved**. The final integration review came back *Ready with
reservations* — all of its non-blocking findings were fixed in `c3c603f`, leaving no
open review item.
Verification: macOS `NonWindows.slnx` 0W/0E; gateway filtered suites green (dashboard
276/276, alarm suites green); `scripts/check-codegen.ps1` 4/4 on macOS. **The full
five-language client matrix ran locally for the first time** — dotnet 133 passed /1
skipped, Go clean, Rust 108, Python 168 passed /1 skipped, Java 131. Windev at `90331b6`:
full `slnx` 0W/0E (one transient MSB4166 node crash, clean on retry), worker x86 523
passed / 11 skipped plus **one pre-existing deterministic failure that reproduces on
`main`** (`WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`),
gateway 1151/1151 on rerun (the known windev load flake), live MXAccess smoke 8/8. The
comment- and prose-only commits landed after `90331b6` are unverified on windev by
design; they need only a cheap tip re-build there.
- **A session-limit outage interrupted four reviewers mid-run.** All four resumed
cleanly on retry; no review was lost or silently truncated.
- **Task 2 surfaced two plan defects, both absorbed rather than deferred.** The spec
claimed the Java CLI renders alarm frames as generic JSON; it is in fact an exhaustive
`switch` that does not compile without an arm for the new case. The spec's work list
also omitted the Java generated-bindings tree.
- **A reviewer's Minor-2 was refuted, and the doc defect behind it fixed instead.** The
finding assumed alarm records coexist after a re-raise; fetch evidence from the probe
rig shows the new GUID replaces the record. The prose that implied coexistence was
corrected rather than the code.
Follow-ups recorded, not started:
- `WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`
fails deterministically on windev **and on `main`** — pre-existing, needs its own
investigation.
- `check-codegen.ps1` Check 4 is unrunnable on Windows: the `protoc-gen-go` version banner
carries a `.exe` suffix that the exact-string compare in
`clients/go/generate-proto.ps1:10,55` does not tolerate.
- Windev has `protoc-gen-go-grpc` 1.6.1 against the repo's pinned 1.6.2.
- `clients/java`'s `checkGeneratedClean` is dead under Gradle 9 (`Project.exec` was
removed); it needs `ExecOperations` injection to work again.
- `SettingsPage` renders every `EffectiveDashboardConfiguration` member except
`RecentFaultLimit` / `RecentSessionLimit` (pre-existing, predates this branch).
- The ack-leg probe stays blocked; unblock paths are in `docs/AlarmProbeFindings.md`.
- The dashboard `AlarmsPage` truncation banner is still poll-driven — it could consume
the new `snapshot_status` feed frame instead.
- **A closed code-review finding regressed, or was never applied.** Server-012
(`code-reviews/Server/findings.md:405-412`) is recorded *Resolved 2026-05-18* and claims
it corrected two scope lists to the canonical `*:*` strings — the `CLAUDE.md`
Build/Test/Run `apikey` sample and the `CLAUDE.md` Authentication-section scope list.
Neither correction was present when this branch looked; both were re-fixed here, along
with a third instance the finding never covered (`docs/Authentication.md`'s `ops.alice`
example). The bookkeeping is the follow-up: other `Server-0xx` entries marked Resolved
with documentation-only fixes should be spot-checked for the same pattern, since a
finding that reads Resolved is not otherwise re-examined.
Explicitly decided, not an omission: **`../scadaproj/CLAUDE.md` needs no update.** The
umbrella index records the *set* of `.proto` files this repo owns, and that set is
unchanged — Task 1 added a message and a field inside an existing proto, not a new
contract file.
@@ -7,9 +7,9 @@
{"id": 4, "subject": "Task 4: Codegen guard — reverse-direction Check 3 + stale-note correction", "status": "completed", "commit": "711793f", "review": "Approved (code review, no issues)"},
{"id": 5, "subject": "Task 5: Settings page — GroupToTag + UntaggedSessionVisibility rows", "status": "completed", "commits": ["094f2ff", "ff16c69"], "review": "Approved; comparer-propagation fix applied and re-verified"},
{"id": 6, "subject": "Task 6: ApiKeysPage — DashboardTags in ConstraintText + create-form input", "status": "completed", "commits": ["c037d99", "fab600d"], "review": "Spec compliant; code review Approved after dedupe/help-text fix, re-review Approved"},
{"id": 7, "subject": "Task 7: Wnwrap alarm-probe retry — secured-write path (windev)", "status": "pending"},
{"id": 8, "subject": "Task 8: Windev full verification", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7]},
{"id": 9, "subject": "Task 9: Bookkeeping — follow-ups closure + plan record", "status": "pending", "blockedBy": [8]}
{"id": 7, "subject": "Task 7: Wnwrap alarm-probe retry — secured-write path (windev)", "status": "completed", "commits": ["90331b6", "540a118"], "review": "Approved after ack-comment fix; Q2 answered (@COUNT = records-in-reply, exact detection refuted), clear/re-raise GUID answered (new instance), ack leg re-blocked (accepted-but-inert)"},
{"id": 8, "subject": "Task 8: Windev full verification", "status": "completed", "verifiedAt": "90331b6", "result": "slnx 0W/0E; worker x86 523 pass + 1 pre-existing deterministic failure (reproduces on main); gateway 1151/1151 on rerun; live smoke 8/8; check-codegen 1-3 pass, Check 4 blocked by pre-existing Windows .exe pin bug (substance validated out-of-band); final comment-only tip re-check pending after bookkeeping", "residual": "pending-external: cheap windev tip re-build covering the comment/prose-only commits landed after 90331b6"},
{"id": 9, "subject": "Task 9: Bookkeeping — follow-ups closure + plan record", "status": "completed", "commits": ["a5f843c", "self (follow-up bookkeeping commit, immediately after a5f843c)"], "review": "None (trivial classification); a5f843c closes the prior plan's follow-up block, appends the as-built record, and corrects the CLAUDE.md apikey sample; the follow-up commit closes two adjacent stale scope lists (CLAUDE.md Authentication section, docs/Authentication.md ops.alice example) and records the regressed Server-012 resolution as a follow-up"}
],
"lastUpdated": "2026-08-17"
"lastUpdated": "2026-08-18"
}
@@ -28513,9 +28513,13 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
/// <summary>
/// One message on the StreamAlarms feed. The stream opens with one
/// `active_alarm` per currently-active alarm, then a single
/// `snapshot_complete`, then a `transition` for every subsequent change.
/// One message on the StreamAlarms feed. The stream opens with a
/// `provider_status` (the current provider mode), then a `snapshot_status`
/// (whether the cached active set may be incomplete), then one `active_alarm`
/// per currently-active alarm, then a single `snapshot_complete`. After that
/// come the live frames: a `transition` for every subsequent alarm change,
/// interleaved with a further `provider_status` on each failover/failback and a
/// further `snapshot_status` on each change of the truncation verdict.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class AlarmFeedMessage : pb::IMessage<AlarmFeedMessage>
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
string alarm_filter_prefix = 2;
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
message AlarmFeedMessage {
oneof payload {
// Part of the initial active-alarm snapshot (ConditionRefresh).
@@ -119,13 +119,21 @@ public sealed class AlarmsHubPublisherTests
},
};
AlarmFeedMessage snapshotStatus = new()
{
SnapshotStatus = new AlarmSnapshotStatus { Truncated = true },
};
CapturingHubContext hubContext = await RunPublisherAsync(
showTagValues: false,
snapshotComplete,
providerStatus);
providerStatus,
snapshotStatus);
Assert.Same(snapshotComplete, hubContext.Sent[0]);
Assert.Same(providerStatus, hubContext.Sent[1]);
Assert.Same(snapshotStatus, hubContext.Sent[2]);
Assert.True(hubContext.Sent[2].SnapshotStatus.Truncated);
}
/// <summary>
@@ -194,11 +194,13 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
// Use the IwwAlarmConsumer (v1) prefix-named methods for the
// lifecycle. Empirically (live dev-rig 2026-05-01) this is the
// only path that lets AlarmAckByName succeed afterwards. The
// v2 Initialize/Register/Subscribe methods on the class
// succeed (return 0) but acks against that consumer state
// return -55.
// lifecycle. Empirically (live dev-rig 2026-05-01, reconfirmed
// 2026-08-18) this is the only path that lets AlarmAckByName
// return rc=0 afterwards. The v2 Initialize/Register/Subscribe
// methods on the class succeed (return 0) but acks against that
// consumer state return -55. Note rc=0 means the call was
// accepted, not that an acknowledgement was applied — see
// AcknowledgeByName below and docs/AlarmProbeFindings.md.
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
if (init != 0)
{
@@ -343,13 +345,28 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
?? throw new InvalidOperationException(
"Cannot acknowledge: WnWrapAlarmConsumer was disposed or has not been subscribed yet.");
// Empirically (live dev-rig 2026-05-01): the IwwAlarmConsumer2
// 8-arg AlarmAckByName returns -55 on this AVEVA build (looks like
// a stub). The legacy 6-arg IwwAlarmConsumer.AlarmAckByName works
// and reaches the alarm-history path correctly. Operator-domain
// and operator-full-name fields are accepted by the proto contract
// for forward-compat but are not propagated to AVEVA today —
// wrapped in the 6-arg call so domain/full-name go to the
// The original observation (live dev-rig 2026-05-01) was narrower than
// it read: the IwwAlarmConsumer2 8-arg AlarmAckByName returned -55
// "and looked like a stub" while the legacy 6-arg
// IwwAlarmConsumer.AlarmAckByName returned 0, which is how the 6-arg
// overload came to be the one called here. The 2026-08-18 probe
// corrected both halves. The -55 tracks the *consumer*, not the
// overload: both overloads return -55 against a
// SetXmlAlarmQuery-applied consumer and 0 against the ack-only one
// used above. And a zero return is NOT evidence the acknowledgement
// was applied — the probe acked a real, freshly raised alarm six ways
// and watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty,
// and the alarm extension's own .Acked attribute stay False for 16 s
// after each call. On that rig the ack is accepted and then inert, so
// whether any wnwrap ack reaches the alarm-history path is
// unverified; see docs/AlarmProbeFindings.md and the 2026-08-18
// correction in docs/AlarmClientDiscovery.md. The 6-arg call site
// below therefore stays as-is for MXAccess parity — the choice is no
// longer justified by the rc, and rc semantics are per the probe.
//
// Operator-domain and operator-full-name fields are accepted by the
// proto contract for forward-compat but are not propagated to AVEVA
// today — wrapped in the 6-arg call so domain/full-name go to the
// alarm-history operator-name field via the szOprName parameter.
// Suppress unused-warning explicitly:
_ = ackOperatorDomain;