feat(gateway): migrate GatewayHost.ResolveSessionType off SessionId.IsSubAgent substring (Phase 5 / F-6 step 2)#557
Merged
Conversation
…nd signal (Phase 5 / F-6 step 2 — closes #554) Replaces the legacy `session.SessionId.IsSubAgent` substring parse in `GatewayHost.ResolveSessionType` with a typed lookup via `IAgentRegistry`: `descriptor.Kind == AgentKind.SubAgent`. When the registry has no descriptor for the agent (transient deregister race or test composition) the host defaults to `SessionType.UserAgent` rather than falling back to the substring — the substring path is now fully cut from this method. The `IAgentRegistry` ctor dependency is optional (nullable, defaults to null) so existing test composition roots continue to compile unchanged. Production DI already registers `DefaultAgentRegistry` (verified in `GatewayServiceCollectionExtensions.cs:112`) so the runtime path always gets the typed signal. Behaviour pinned by 7 new tests in `GatewayHostResolveSessionTypeTests.cs` covering each row of the truth table: - SubAgent descriptor → AgentSubAgent - Named descriptor + sub-agent-shaped SessionId → UserAgent (typed signal trumps substring) - No descriptor + sub-agent-shaped SessionId → UserAgent (no fallback to substring) - No registry at all + sub-agent-shaped SessionId → UserAgent - Soul SessionId → Soul (regression pin) - Cron channel type → Cron (regression pin) - SubAgent descriptor + cron channel → AgentSubAgent (precedence pin) Architecture fence updated: `GatewayHost.cs` allowlist entry removed from `AgentKindArchitectureTests`; the remaining DEFERRED entry is `SessionsController` (#555) which needs a persisted `SessionType` discriminator on the session row before its read-side filter can be cut. Closes #554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ster; cache cron ChannelKey Critique sweep amendments for the #554 ResolveSessionType migration. Adopted from bug-hunt critique (gpt-5.3-codex): - HIGH: persisted AgentSubAgent sessions could be silently downgraded to UserAgent on next dispatch when the sub-agent descriptor had been Unregister()'d (sub-agent completion) or was transiently absent after a gateway restart. This would flip Session.IsInteractive and warmup / memory-flush gating downstream. Fix: thread isNewSession into ResolveSessionType and preserve AgentSubAgent on EXISTING sessions when no typed signal contradicts. This deliberately requires isNewSession=false so fresh dispatches do NOT re-open a substring back door — the #554 architecture-fence invariant stays intact. - MEDIUM: test suite missed the downgrade regression path. Fix: added DispatchAsync_WhenSessionAlreadyPersistedAsAgentSubAgent_ AndRegistryHasNoDescriptor_PreservesAgentSubAgent — seeds an existing AgentSubAgent session via GetOrCreateAsync, then re-dispatches with a null registry and asserts the type is preserved. - LOW: ChannelKey.From("cron") allocated on every dispatch. Fix: cached as private static readonly CronChannelKey. Adopted from plan-vs-impl critique (claude-opus-4.7): - Missing truth-table baseline row. Fix: added DispatchAsync_WhenRegistryReturnsNamedDescriptor_ AndSessionIdIsPlain_StampsAsUserAgent. - Fragile test literal containing the word 'subagent'. Fix: renamed 'session-with-subagent-descriptor' to 'plain-session-id-1' so the SubAgent classification can only come from the descriptor. Skipped from plan-vs-impl critique: - 'Drop redundant test at L104'. The two tests at L84 / L104 pin subtly different DI scenarios (null returned from registry vs. registry not injected at all). Both worth keeping. Security review (gpt-5.5): PASS — no vulnerabilities, no changes needed. Validation: - Build: 0 warnings, 0 errors under TreatWarningsAsErrors. - GatewayHostResolveSessionTypeTests: 9/9 passed (7 original + 2 new). - BotNexus.Gateway.Tests: 1815/1816 passed (1 skipped). - Architecture fence: 49/49 passed. - Full solution suite: ~4,600 passed / 41 skipped / 0 failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 5 / F-6 step 2: migrate
GatewayHost.ResolveSessionTypeoff the legacySessionId.IsSubAgentsubstring check. The sub-agent session-type discriminator is now driven by the typedAgentDescriptor.Kindsignal sourced fromIAgentRegistry. Closes #554.This is the second migration step after PR #556 (which introduced
AgentKind). The substring path stays alive only in the architecturally-allowlisted call sites:SessionStoreBase.InferSessionType(legacy read path),InProcessIsolationStrategy(defense-in-depth OR gate), andSessionsController(deferred to #555).Changes
src/gateway/BotNexus.Gateway/GatewayHost.csResolveSessionTypeis now an instance method that reads_registry?.Get(session.AgentId)?.Kindinstead ofsession.SessionId.IsSubAgent. New optionalIAgentRegistry? registry = nullctor parameter (preserves back-compat with the 7 test composition roots). CachedCronChannelKeystatic field to remove per-dispatch allocation. NewisNewSessionparameter to enable the persisted-AgentSubAgent preservation rule (see Critique below).tests/architecture/BotNexus.Architecture.Tests/AgentKindArchitectureTests.csGatewayHost.csallowlist entry. Rewrote XML doc remarks to record migration history and call outSessionsController(#555) as the remaining deferred site.tests/gateway/BotNexus.Gateway.Tests/GatewayHostResolveSessionTypeTests.csCritique sweep — 3 background agents
Following the standing multi-model critique policy. All three completed before commit-amendment.
security-reviewgpt-5.5code-reviewplan-vs-implclaude-opus-4.7(after4.6returned 401)rubber-duckbug-huntgpt-5.3-codexHIGH adopted (production-bug-prevention)
Fix: threaded
isNewSessionintoResolveSessionTypeand added a fail-closed branch that preservesAgentSubAgenton EXISTING sessions when no typed signal contradicts. The branch deliberately requiresisNewSession=falseso fresh dispatches do NOT re-open a substring back door — the #554 architecture-fence invariant stays intact.Regression test:
DispatchAsync_WhenSessionAlreadyPersistedAsAgentSubAgent_AndRegistryHasNoDescriptor_PreservesAgentSubAgentseeds an existingAgentSubAgentsession viaGetOrCreateAsync, then re-dispatches with a null registry and asserts the type is preserved.LOW adopted
ChannelKey.From("cron")allocated per dispatch → cached asprivate static readonly CronChannelKey.Plan-vs-impl adopted
DispatchAsync_WhenRegistryReturnsNamedDescriptor_AndSessionIdIsPlain_StampsAsUserAgent."session-with-subagent-descriptor"→"plain-session-id-1"so the SubAgent classification can only come from the typed descriptor, not from any accidental substring match.Truth table (pinned by tests)
AgentSubAgent…ReturnsSubAgentDescriptor_StampsSessionTypeAsAgentSubAgent::subagent::infixUserAgent(typed signal trumps substring)…ReturnsNamedDescriptor_AndSessionIdHasSubAgentInfix_StampsAsUserAgent⭐ critical pin::subagent::infixUserAgent(no substring fallback)…RegistryHasNoDescriptor_AndSessionIdHasSubAgentInfix_StampsAsUserAgent::subagent::infixUserAgent(no substring fallback)…RegistryNotProvided_AndSessionIdHasSubAgentInfix_StampsAsUserAgentAgentSubAgent(preserved)…SessionAlreadyPersistedAsAgentSubAgent_…_PreservesAgentSubAgent⭐ regression pinUserAgent(baseline)…ReturnsNamedDescriptor_AndSessionIdIsPlain_StampsAsUserAgentSoul…WhenSessionIdIsSoul_StampsAsSoulCron…WhenChannelTypeIsCron_StampsAsCronAgentSubAgent(precedence)…WhenChannelTypeIsCron_AndDescriptorKindIsSubAgent_PrefersSubAgentBucketingValidation
TreatWarningsAsErrors.GatewayHostResolveSessionTypeTests: 9/9 passed (7 original + 2 added during critique fold-in).BotNexus.Gateway.Tests: 1815/1816 passed (1 skipped, 0 failed).AgentKindArchitectureTests): 49/49 passed.Follow-up
SessionsControllersubstring filter migration. Bigger scope (~200–400 LoC; requires persisting aSessionTypediscriminator on the session list query). Will be the next PR in the Phase 5 / F-6 sequence.Closes #554