# Cluster 03 — Sessions/Runtime Auditor: automated (claude-sonnet-4-6) Date: 2026-06-03 Source doc: docs/Sessions.md Verified against: src/ZB.MOM.WW.MxGateway.Server/Sessions/**, src/ZB.MOM.WW.MxGateway.Server/Workers/** --- DOC / LINES / 9 CLAIM: "All four interfaces (`ISessionManager`, `ISessionRegistry`, `ISessionWorkerClientFactory`) plus `SessionShutdownHostedService` are wired as singletons by `SessionServiceCollectionExtensions.AddGatewaySessions`." CLAIM_TYPE: term VERDICT: wrong EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs:9-18 — only three interfaces exist (confirmed by `ls I*.cs` in Sessions/). The doc claims "four interfaces" but names only three. Additionally the DI registration also registers `SessionLeaseMonitorHostedService` as a hosted service, which is omitted from this sentence. CODE_AREA: session.di SEVERITY: medium PROPOSED_FIX: Change "All four interfaces" to "All three interfaces". Separately note that two hosted services are registered: `SessionLeaseMonitorHostedService` and `SessionShutdownHostedService`. --- DOC / LINES / 265-276 CLAIM: Code snippet for `AddGatewaySessions` shows only `SessionShutdownHostedService` registered; `SessionLeaseMonitorHostedService` is absent from the snippet. CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs:14-15 — actual code registers both `AddHostedService()` and `AddHostedService()`. The snippet in the doc is missing the lease-monitor line. CODE_AREA: session.di SEVERITY: medium PROPOSED_FIX: Add `services.AddHostedService();` to the code snippet (between the `ISessionManager` singleton line and the shutdown service line). --- DOC / LINES / 232-259 CLAIM: The `ShutdownAsync` code snippet shown calls `session.KillWorker(GatewayShutdownReason)` and `await RemoveSessionAsync(session)` directly in the catch block. CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:296-331 — the actual `ShutdownAsync` fallback calls `await KillWorkerAsync(session.SessionId, GatewayShutdownReason, cancellationToken)` (which routes through `KillWorkerWithCloseGateAsync` and then `RemoveSessionAsync`), not a direct `session.KillWorker` + `RemoveSessionAsync`. The old snippet predates the Server-045/Server-046 refactor that unified the kill path through `KillWorkerAsync`. CODE_AREA: session.shutdown SEVERITY: medium PROPOSED_FIX: Replace the ShutdownAsync snippet with the current implementation, which checks `_registry.TryGet` then calls `KillWorkerAsync` (wrapped in its own try/catch) instead of directly calling `session.KillWorker` and `RemoveSessionAsync`. --- DOC / LINES / 55-59 CLAIM: "`KillWorkerAsync` is the forceful path used by the dashboard's admin Kill button: it calls `GatewaySession.KillWorker` directly, which kills the worker process immediately with no graceful-shutdown attempt and transitions the session to `Closed`." CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:216-264 — `KillWorkerAsync` now calls `session.KillWorkerWithCloseGateAsync` (not `GatewaySession.KillWorker` directly). The `KillWorkerWithCloseGateAsync` method acquires `_closeLock` before killing, serializing concurrent close/kill attempts (Server-045 fix). The old description of a direct `KillWorker` call is stale. CODE_AREA: session.lifecycle SEVERITY: medium PROPOSED_FIX: Update description to state that `KillWorkerAsync` calls `session.KillWorkerWithCloseGateAsync`, which acquires the per-session close lock before killing the worker, so concurrent close and kill callers serialize. --- DOC / LINES / 59 CLAIM: "Both paths converge on the same registry/metrics cleanup, so the open-session slot is released and `mxgateway.sessions.closed` is incremented either way." CLAIM_TYPE: config-key VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs:59 — counter name `mxgateway.sessions.closed` confirmed. Both `CloseSessionCoreAsync` and `KillWorkerAsync` call `_metrics.SessionClosed()` and `RemoveSessionAsync` (which calls `ReleaseSessionSlot`). CODE_AREA: session.metrics SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 60-72 CLAIM: Code snippet for `EnsureSessionCapacity` throws `SessionManagerException` with `SessionLimitExceeded`; open requests that exceed the bound "throw ... rather than queuing". CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:388-396 — `_sessionSlots.Wait(0)` (zero timeout = non-blocking) confirms the no-queue, immediate-throw behavior. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 61 CLAIM: "Concurrency is bounded by a `SemaphoreSlim` initialized to `GatewayOptions.Sessions.MaxSessions`." CLAIM_TYPE: config-key VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:53 — `new SemaphoreSlim(_options.Sessions.MaxSessions, _options.Sessions.MaxSessions)`. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 75 CLAIM: "three close-reason constants — `DefaultCloseReason` (`\"client-close\"`), `GatewayShutdownReason` (`\"gateway-shutdown\"`), and `LeaseExpiredReason` (`\"lease-expired\"`)" CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:17-19 — all three constants confirmed with exact string values. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 79-81 CLAIM: "`SessionRegistry` is a thin wrapper over a `ConcurrentDictionary` keyed by session id with `StringComparer.Ordinal`." CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs:12 — `new ConcurrentDictionary(StringComparer.Ordinal)` confirmed. CODE_AREA: session.registry SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 81 CLAIM: "`ActiveCount` filters out sessions whose state is `Closed`" CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs:22 — `_sessions.Values.Count(session => session.State is not SessionState.Closed)` confirmed. CODE_AREA: session.registry SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 15-19 CLAIM: "The session id is an opaque string in the form `session-{guid:N}` and the per-session pipe name is `mxaccess-gateway-{ProcessId}-{SessionId}`." CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:433 (`pipeName = $"mxaccess-gateway-{Environment.ProcessId}-{sessionId}"`) and :479 (`$"session-{Guid.NewGuid():N}"`). CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 19 CLAIM: "`SessionState` itself is the protobuf-generated enum from `ZB.MOM.WW.MxGateway.Contracts.Proto`" CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs:1 — `using ZB.MOM.WW.MxGateway.Contracts.Proto;` and the state field is typed `SessionState`. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 85-87 CLAIM: "`SessionWorkerClientFactory.CreateAsync` … drives the session through the protobuf `SessionState` substates in order: `StartingWorker`, `WaitingForPipe`, `Handshaking`, `InitializingWorker`." CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs:60-105 — `TransitionTo(SessionState.StartingWorker)` → `TransitionTo(SessionState.WaitingForPipe)` → `TransitionTo(SessionState.Handshaking)` → `TransitionTo(SessionState.InitializingWorker)` in sequence. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 87-98 CLAIM: Startup timeout wrapped as `TimeoutException` with the exact catch pattern shown — `OperationCanceledException` where `startupCancellation.IsCancellationRequested` and `!cancellationToken.IsCancellationRequested`. CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs:145-153 — identical predicate confirmed. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 100 CLAIM: "The named pipe is created with `maxNumberOfServerInstances: 1`" CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs:166 — `maxNumberOfServerInstances: 1` confirmed. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 104 CLAIM: "`SessionShutdownHostedService` … catches `OperationCanceledException` triggered by the host shutdown timeout and logs a warning so that an over-running shutdown does not surface as an unhandled exception." CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionShutdownHostedService.cs:18-28 — exact catch confirmed. CODE_AREA: session.shutdown SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 109-127 CLAIM: `SessionOpenRequest` is a `sealed record` with fields `RequestedBackend`, `ClientSessionName`, `ClientCorrelationId`, `CommandTimeout`, and a `FromContract` factory. CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionOpenRequest.cs:6-24 — confirmed. Note: the doc snippet includes a `ClientCorrelationId` field in the record definition, but the actual `SessionManager.CreateSession` derives `clientCorrelationId` internally rather than forwarding the field from the request. This is a minor mismatch between what the record holds vs. how it is used, but does not constitute an error in the doc's description of the record type itself. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 134-139 CLAIM: `SessionCloseResult` is a `sealed record` with `SessionId`, `FinalState`, `AlreadyClosed`. CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionCloseResult.cs:5-8 — confirmed. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 143 CLAIM: "`SessionCloseStartedException` is `internal`" CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionCloseStartedException.cs:3 — `internal sealed class SessionCloseStartedException` confirmed. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 148-157 CLAIM: Error code table for `SessionManagerException` — seven codes listed: `SessionNotFound`, `SessionNotReady`, `EventSubscriberAlreadyActive`, `EventQueueOverflow`, `SessionLimitExceeded`, `OpenFailed`, `CloseFailed`. CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManagerErrorCode.cs:1-12 — all seven members confirmed in order. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 163-188 CLAIM: Open failure rollback order: "fault, deregister, dispose, release slot, record metric, log, rethrow". CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:97-123 — actual order is: MarkFaulted → TryRemove (deregister) → DisposeAsync → (conditionally) SessionRemoved metric if sessionOpenedRecorded → ReleaseSessionSlot → Fault metric → LogWarning → rethrow. The doc omits the `sessionOpenedRecorded` conditional `SessionRemoved()` call that was added in the Server-006 fix, making the described order incomplete. The doc text says "release slot, record metric" but the actual code calls `SessionRemoved` before `ReleaseSessionSlot` when `sessionOpenedRecorded` is true. CODE_AREA: session.lifecycle SEVERITY: medium PROPOSED_FIX: Update the rollback description to note the conditional `SessionRemoved()` metric call that precedes `ReleaseSessionSlot` when `SessionOpened()` was already recorded (guards against mxgateway.sessions.open gauge leak on late failures such as auto-subscribe rejection). --- DOC / LINES / 193-195 CLAIM: "`GatewaySession` also exposes typed bulk helpers (`AddItemBulkAsync`, `SubscribeBulkAsync`, etc.) that wrap `WorkerCommand` round-trips and translate non-`Ok` `ProtocolStatus` replies into `SessionManagerException` with `SessionNotReady`." CLAIM_TYPE: term VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs:490, 590 (AddItemBulkAsync, SubscribeBulkAsync) and :1017-1023 (ProtocolStatusCode.Ok guard throwing SessionManagerException(SessionNotReady)). CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 195-197 CLAIM: "Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel. Active event subscribers keep the session lease from expiring until the stream is disposed." CLAIM_TYPE: behavior-rule VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs:387-407 (AttachEventSubscriber guard and lease) and :373-380 (IsLeaseExpired checks `_activeEventSubscriberCount == 0`). CODE_AREA: session.subscriber SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 197 CLAIM: "Sessions open with `MxGateway:Sessions:DefaultLeaseSeconds` (default 1800)" CLAIM_TYPE: config-key VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs:21 — `public int DefaultLeaseSeconds { get; init; } = 1800`. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 197 CLAIM: "`SessionLeaseMonitorHostedService` runs that sweep every `MxGateway:Sessions:LeaseSweepIntervalSeconds` seconds (default 30)." CLAIM_TYPE: config-key VERDICT: accurate EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs:24 — `public int LeaseSweepIntervalSeconds { get; init; } = 30`; src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionLeaseMonitorHostedService.cs:19 — `TimeSpan.FromSeconds(Math.Max(1, options.Value.Sessions.LeaseSweepIntervalSeconds))`. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: flag only --- DOC / LINES / 230 CLAIM: "`GatewaySession.KillWorker` is the unconditional forced-close path used by shutdown when graceful close itself throws, and also by `SessionManager.KillWorkerAsync` — the explicit kill path that the dashboard's admin Kill button invokes." CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:233 — `KillWorkerAsync` now calls `session.KillWorkerWithCloseGateAsync` (not `session.KillWorker`). The shutdown fallback (line 319) also routes through `KillWorkerAsync` rather than calling `session.KillWorker` + `RemoveSessionAsync` directly. `GatewaySession.KillWorker` is still present (line 874) but is no longer the entry point from `SessionManager.KillWorkerAsync`. CODE_AREA: session.lifecycle SEVERITY: medium PROPOSED_FIX: Update to reflect that `SessionManager.KillWorkerAsync` delegates to `session.KillWorkerWithCloseGateAsync` (which serializes concurrent kill/close via `_closeLock` — Server-045 fix) and that `GatewaySession.KillWorker` is now only the internal terminal action inside `KillWorkerWithCloseGateAsync`. --- DOC / LINES / 230 CLAIM: "`KillCount` increments while `ShutdownCount` does not" CLAIM_TYPE: term VERDICT: wrong EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs:56-79 — no metrics named `KillCount` or `ShutdownCount` exist. The actual worker-kill metric is `mxgateway.workers.killed` (counter). The doc invents non-existent metric names. CODE_AREA: session.metrics SEVERITY: high PROPOSED_FIX: Replace "KillCount increments while ShutdownCount does not" with "the `mxgateway.workers.killed` counter is incremented (via `GatewayMetrics.WorkerKilled`) while the graceful-shutdown path does not increment it". --- DOC / LINES / 265 CLAIM: "registers the four singletons and the hosted service" (singular "the hosted service") CLAIM_TYPE: behavior-rule VERDICT: wrong EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs:14-15 — two hosted services are registered: `SessionLeaseMonitorHostedService` and `SessionShutdownHostedService`. CODE_AREA: session.di SEVERITY: medium PROPOSED_FIX: Change "registers the four singletons and the hosted service" to "registers the three singletons and two hosted services (`SessionLeaseMonitorHostedService`, `SessionShutdownHostedService`)". --- DOC / LINES / 279 CLAIM: "Registering `SessionShutdownHostedService` last ensures it is constructed after `ISessionManager` and therefore drains sessions during host stop." CLAIM_TYPE: behavior-rule VERDICT: stale EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs:14-15 — `SessionLeaseMonitorHostedService` is now registered before `SessionShutdownHostedService`. The shutdown service is still last of the two hosted services, but the reasoning in the doc no longer fully applies because construction order of hosted services relative to singletons is governed by ASP.NET Core's DI container, not purely registration order. CODE_AREA: session.di SEVERITY: low PROPOSED_FIX: Update to note that two hosted services are registered in order (lease monitor first, shutdown second) and that both depend on `ISessionManager` which is registered as a singleton. --- DOC / LINES / (none — gap) CLAIM: (gap) `GatewaySession` holds an item registration dictionary (`_items`, keyed by `(ServerHandle, ItemHandle)`) tracking all successfully added/subscribed items. The session tracks and prunes these registrations via `TrackCommandReply`, `TryGetItemRegistration`, and the per-command `TrackItem`/`RemoveItems` helpers. This bookkeeping is undocumented. CLAIM_TYPE: behavior-rule VERDICT: gap EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs:17 (_items field), :425-481 (TrackCommandReply), :1059-1090 (TrackItem, TrackBulkItems, RemoveItems). src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionItemRegistration.cs:3 (SessionItemRegistration record). CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: Add a subsection or paragraph noting that `GatewaySession` maintains an in-session item registry keyed by `(ServerHandle, ItemHandle)`, updated after successful `AddItem`, `AddItem2`, `AddBufferedItem`, `AddItemBulk`, `SubscribeBulk`, `RemoveItem`, `RemoveItemBulk`, and `UnsubscribeBulk` replies. --- DOC / LINES / (none — gap) CLAIM: (gap) `SessionOptions` exposes `AllowMultipleEventSubscribers` (default `false`). Setting it `true` is **rejected at startup** by `GatewayOptionsValidator` with the message "AllowMultipleEventSubscribers is not supported until event fan-out is implemented." This validator-level enforcement of the v1 constraint is undocumented. CLAIM_TYPE: config-key VERDICT: gap EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs:29 and src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs:181-184. CODE_AREA: session.subscriber SEVERITY: medium PROPOSED_FIX: Add a note to the "Run" section explaining that `MxGateway:Sessions:AllowMultipleEventSubscribers` exists but is actively refused by the validator in v1; operators who set it to `true` will see a startup validation failure, not a runtime error. --- DOC / LINES / (none — gap) CLAIM: (gap) Gateway-restart orphan cleanup is performed by `OrphanWorkerCleanupHostedService` (wrapping `OrphanWorkerTerminator.TerminateOrphans`) on `StartAsync`, before the gateway accepts sessions. Cleanup is best-effort (a failure logs a warning but does not block startup). The `Sessions.md` doc does not mention this, yet it directly affects the "gateway restart does not reattach orphan workers" contract. CLAIM_TYPE: behavior-rule VERDICT: gap EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerCleanupHostedService.cs:7-30; src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerTerminator.cs:49-95; src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerServiceCollectionExtensions.cs:19. CODE_AREA: session.orphan SEVERITY: high PROPOSED_FIX: Add a "Gateway Restart / Orphan Cleanup" section to Sessions.md (or cross-reference from Shutdown Coordination) noting that `OrphanWorkerCleanupHostedService` runs `OrphanWorkerTerminator.TerminateOrphans` on startup, kills any running worker executables matching the configured `MxGateway:Worker:ExecutablePath`, and that failures are non-fatal to startup. --- DOC / LINES / (none — gap) CLAIM: (gap) `SessionOptions.MaxPendingCommandsPerSession` (default 128) is passed to `WorkerClientOptions.MaxPendingCommands` during session construction. This per-session command concurrency cap is not documented in Sessions.md. CLAIM_TYPE: config-key VERDICT: gap EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs:18; src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs:92. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: Add a note in the "Key Types — SessionManager" or "Run" section that each session is bounded to `MxGateway:Sessions:MaxPendingCommandsPerSession` (default 128) concurrent in-flight worker commands. --- DOC / LINES / (none — gap) CLAIM: (gap) `GatewaySession` exposes a `KillWorkerWithCloseGateAsync` method that acquires `_closeLock` before killing, introduced to serialize concurrent close/kill callers (Server-045). This method is not mentioned; the doc describes only `KillWorker` as the unconditional kill path from `SessionManager`. CLAIM_TYPE: term VERDICT: gap EVIDENCE: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs:896-917; src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:233. CODE_AREA: session.lifecycle SEVERITY: low PROPOSED_FIX: Mention `KillWorkerWithCloseGateAsync` in the "Close" section as the locked kill path now used by `SessionManager.KillWorkerAsync`, distinguishing it from the bare `KillWorker` still used as the internal terminal action.