[WIP] Port nanobot to C# with extensible environment support#1
Conversation
- Full solution structure with 15 projects targeting .NET 10 - BotNexus.Core: abstractions (ILlmProvider, IChannel, ISessionManager, etc.), models, configuration, message bus - BotNexus.Session: file-backed JSONL session persistence - BotNexus.Agent: AgentLoop, AgentRunner, ContextBuilder, MemoryStore, SkillsLoader, tools (filesystem, shell, web, message, cron) - BotNexus.Command: priority-based command router with built-in /help /reset /status - BotNexus.Channels.Base: BaseChannel abstract class, ChannelManager - BotNexus.Providers.Base: LlmProviderBase with retry logic, ProviderRegistry - BotNexus.Providers.OpenAI: full OpenAI SDK integration with streaming - BotNexus.Providers.Anthropic: Anthropic Messages API via HttpClient - BotNexus.Channels.Telegram/Discord/Slack: channel implementations - BotNexus.Cron: Cronos-based cron scheduling as IHostedService - BotNexus.Heartbeat: heartbeat service as IHostedService - BotNexus.Gateway: main hosted service wiring everything together - BotNexus.Api: OpenAI-compatible REST API - 61 passing tests (58 unit + 3 integration) - GitHub Actions CI workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: sytone <1399443+sytone@users.noreply.github.com>
…brary Agent-Logs-Url: https://github.com/sytone/botnexus/sessions/1c7ed4f9-1b63-4c4f-bf8e-f630ef53d64b Co-authored-by: sytone <1399443+sytone@users.noreply.github.com>
- Switch gateway from Worker SDK to Web SDK (enables Kestrel + WebSockets) - Add WebSocketEnabled / WebSocketPath to GatewayConfig - Add WebSocketChannel: IChannel that routes agent responses to connected sockets via per-connection Channel<string> response queues - Add GatewayWebSocketHandler: accepts upgrades on /ws, publishes inbound messages to IMessageBus, streams responses back to client - Program.cs: WebApplication with Kestrel on GatewayConfig.Host:Port, /health endpoint, /ws branch - Register WebSocketChannel + GatewayWebSocketHandler in BotNexusServiceExtensions - Add 12 unit tests for WebSocketChannel Agent-Logs-Url: https://github.com/sytone/botnexus/sessions/03c18968-8be4-4db7-9bed-7eaf12079ae7 Co-authored-by: sytone <1399443+sytone@users.noreply.github.com>
…hanced WebSocket monitor mode
- Add IActivityStream/ActivityStream to Core for broadcasting system-wide events
- Add ActivityEvent model for message received/response sent/delta/error events
- Add REST API endpoints: GET /api/sessions, /api/sessions/{key}, /api/channels, /api/agents
- Enhance Gateway to publish activity events on message dispatch and errors
- Enhance GatewayWebSocketHandler to support 'subscribe' message type for monitoring all traffic
- Enhance WebSocketChannel to publish outbound activity events
- Create BotNexus.WebUI project with static HTML/CSS/JS chat UI
- Web UI features: session list, agent list, channel status, activity monitor, real-time chat
- Add 10 unit tests for ActivityStream
- All 124 tests pass
Agent-Logs-Url: https://github.com/sytone/botnexus/sessions/a15b4908-b6b1-4463-9d9d-d4817cd0b9bd
Co-authored-by: sytone <1399443+sytone@users.noreply.github.com>
sytone
left a comment
There was a problem hiding this comment.
Not overly concerned of initial check-in this is just to get the framework based structure in place for a deeper review and local development environment execution
**Issue #1: Config write safety** - ConfigFileManager.SaveConfig now uses JsonNode for surgical updates - Preserves all fields not in C# model (prevents data loss) - Deep merge strategy ensures provider-specific fields survive **Issue #2: Auto-reauth on token expiry** - CopilotProvider now clears ALL tokens on 401/403 (GitHub OAuth + Copilot access) - Automatic retry with fresh auth after token exchange failure - Clear error messages guide user to re-authenticate - Prevents silent failures when token is missing/expired **Fixes:** - Added missing using directive (BotNexus.Core.Bus) to Program.cs - Fixed null reference warning in CopilotProvider.ChatCoreAsync - Updated TestActivityStream mock to implement new IActivityStream method All Copilot provider tests passing (6/6). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add IMediaHandler to additive DI registration branch (was TryAddSingleton) - Add botnexus-extension.json manifest for Whisper extension discovery - Copy manifest to output directory via Content item - Fixes: Nibbler consistency review issues #1 and #2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ecisions Multi-agent coordination session for issue #24 (Tool Timeout Configuration & Auto-Cancel). Completed Phase 1 delivery with requirements #1, #2, #3, #8 (config wiring, runtime propagation, structured logging). Follow-ups tracked separately (stuck-turn, user cancel, auto-cancel, UI health). Changes: - Added orchestration logs for all spawned agents (Leela, Hermes, Bender, Nibbler, Coordinator) - Added session log for issue #24 multi-agent coordination and outcomes - Merged decision inbox files into decisions.md, deleted inbox files - Updated agent history files with issue #24 session outcomes - Created decisions-archive.md placeholder for future archival (decisions.md: 739KB > 20KB threshold) Orchestration Outcomes: - Leela: Architecture review approved (minimal, correct, well-bounded) - Hermes: TDD test contracts locked and verified (3 regression tests passing) - Bender: Runtime implementation complete (config→descriptor→runtime wiring) - Nibbler: Consistency review verified (docs aligned, no cross-agent drift) - Coordinator: PR submitted (#177) Test Coverage: All 24 issue-related tests passing (9 AgentCore + 15 Gateway) PR Status: Ready for merge (awaiting CI) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(gateway): add issue #24 timeout wiring regressions Add regression tests for missing tool-timeout configuration plumbing from platform config through in-process runtime setup, and add an AgentCore timeout end-event coverage test.\n\nThese tests intentionally document current gaps for issue #24 while preserving existing timeout recovery assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(gateway): wire tool timeout config into runtime (#24) Flow toolTimeoutSeconds from agent/default config into descriptor metadata and apply it as AgentOptions.ToolTimeout in in-process runtime creation. Add inline agents.defaults fallback for direct PlatformConfig construction and structured logging for invalid timeout metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(config): add ToolTimeoutSeconds to config reference tables (#24) - Add ToolTimeoutSeconds to AgentDefaults and per-agent property tables in docs/configuration.md to match new AgentDefaultsConfig and AgentDefinitionConfig properties added by this branch. - Update design spec status from draft to in-progress since config wiring is now implemented (stuck-turn and cancel still pending). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(sessions): update documentation for session store implementations Co-authored-by: Copilot <copilot@github.com> * docs(.squad): Scribe session 2026-05-07 - Issue #24 orchestration & decisions Multi-agent coordination session for issue #24 (Tool Timeout Configuration & Auto-Cancel). Completed Phase 1 delivery with requirements #1, #2, #3, #8 (config wiring, runtime propagation, structured logging). Follow-ups tracked separately (stuck-turn, user cancel, auto-cancel, UI health). Changes: - Added orchestration logs for all spawned agents (Leela, Hermes, Bender, Nibbler, Coordinator) - Added session log for issue #24 multi-agent coordination and outcomes - Merged decision inbox files into decisions.md, deleted inbox files - Updated agent history files with issue #24 session outcomes - Created decisions-archive.md placeholder for future archival (decisions.md: 739KB > 20KB threshold) Orchestration Outcomes: - Leela: Architecture review approved (minimal, correct, well-bounded) - Hermes: TDD test contracts locked and verified (3 regression tests passing) - Bender: Runtime implementation complete (config→descriptor→runtime wiring) - Nibbler: Consistency review verified (docs aligned, no cross-agent drift) - Coordinator: PR submitted (#177) Test Coverage: All 24 issue-related tests passing (9 AgentCore + 15 Gateway) PR Status: Ready for merge (awaiting CI) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(.squad): record PR 184 refresh 2 status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(.squad): fix branch name formatting in PR 184 notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>
Three independent issues surfaced by the PR #549 multi-model critique sweep — security/gpt-5.5, plan-vs-impl/claude-opus-4.6, and bug-hunt/gpt-5.3-codex — fixed together since they share the receiver controller and its test surface. 1. MetadataString silent failure (bug-hunt BLOCKING #1). The 'value as string' cast returned null when SqliteSessionStore / FileSessionStore deserialized object? metadata values as JsonElement (System.Text.Json default for object?). This drove every reused cross-world session past the 'value is null' guard into a one-character-fast-path-fail, then 409 — symptom looked like a stale session being rejected for missing ownership, but actually the controller couldn't see ownership it should have. Switch to the established codebase pattern (string | JsonElement(String) | _) already used by AgentConverseTool and PreCompactionMemoryFlusher. 2. Sealed-session silent reactivation (bug-hunt BLOCKING #5). ResolveSessionAsync reuse branch did not check Session.Status. A sealed session would be 'reused' for new turns even though the archive contract says sealed = terminal. Add an explicit GatewaySessionStatus.Sealed guard that returns 409 with a clear reason; pinned by RelayAsync_WhenRemoteSessionIdSupplied_ButSessionIsSealed_Returns409. 3. Agent-enumeration oracle (security/gpt-5.5 LOW). registry.Contains(targetAgentId) ran BEFORE inboundAuthService.TryAuthorize, so an unauthenticated probe against an unknown agent name returned 404 while a known agent name returned 401 — leaking the agent list to anyone with the relay endpoint. Reorder: authorise the peer first, return 401 uniformly for unauthenticated probes regardless of agent existence. Pinned by RelayAsync_WithInvalidApiKey_AndUnknownTargetAgent_Returns401_NotEnumerable404. Adds 7 critique-sweep tests including isolated coverage for each OwnedByRequester invariant guard (SessionType mismatch, sourceAgentId mismatch, missing conversation, no-bound-conversation) and the JsonElement-metadata reuse regression that proves fix (1). Refs PR #549 critique sweep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sionStore System.Text.Json deserializes Dictionary<string, object?> values as JsonElement (not their original primitive types). This affects both SqliteSessionStore AND FileSessionStore — readers must handle either shape (string OR JsonElement(String)). Add a docs-pin test that writes a Session.Metadata string value and asserts the reload is either a string or a JsonElement(String) — both shapes are acceptable, but downstream code must not assume one or the other (regression for the bug fixed in CrossWorldFederationController. MetadataString). Refs PR #549 critique sweep (bug-hunt BLOCKING #1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ore + delete dead communicator + remove SessionId factories (#549) * feat(gateway): route cross-world receiver through IConversationStore Phase 4 / F-3 (receiver branch). Mirrors PR #548 (sender) on the `CrossWorldFederationController.RelayAsync` path so received cross-world messages now create a real `Conversation` row owned by the local target agent and the receiver session is pinned to it BEFORE the supervisor is invoked (persist-before-prompt — same race window the sender PR closed). Closes the F-6-shaped orphan window on the receiver: previously the relay allocated a synthetic `SessionId.ForAgentConversation` and never touched `IConversationStore` at all, so a background flush or portal page-load during the supervisor turn could see the session with no conversation. Source identity (`SourceWorldId`, `SourceAgentId`, sender-side conversation/session ids) is stashed on `Conversation.Metadata` only — NOT promoted into `Conversation.Title` or `Conversation.Purpose`, both of which `SystemPromptBuilder.BuildConversationContextSection` (`SystemPromptBuilder.cs:601`) injects into the target system prompt. Title is the constant `Cross-world agent exchange`; Purpose is null. Caller-controlled strings in those positions would be an XPIA vector. Session reuse: if the sender supplies `RemoteSessionId` from a previous turn, the receiver validates the supplied id (a) exists (404 if not), (b) is owned by the target agent (409), (c) is a cross-world AgentAgent session (409), and (d) was originally minted for the same `(SourceWorldId, SourceAgentId)` pair (409). Without these checks any world that can guess a session id could impersonate another world's transcript. 15 RED-then-GREEN tests (replace the existing 2) covering the full contract: kind=AgentAgent + Initiator=null, XPIA defence (Title + Purpose), persist-before-prompt ordering pin via RecordingSessionStore, eager-pin assignment, returning local sessionId (not echoing sender's), reuse path with valid RemoteSessionId, all four 409 invariant violations plus 404 on missing, and failure paths (prompt throws + supervisor throws). InMemorySessionStore is sealed so the recorder composes via SessionStoreBase rather than inheriting. Refs #523. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(architecture): pin Phase 4 / 1b receiver shape with 5 fences Mirrors AgentExchangeConversationArchitectureTests (sender, PR #548) for the cross-world receiver. CrossWorldFederationController.cs may not: 1. Call SessionId.ForAgentConversation(...) — the synthetic-encoding factory the receiver was built on must stay deleted from this path. 2. Lose its IConversationStore namespace import. 3. Accept a ctor without an IConversationStore dependency. 4. Have any .ConversationId = assignment lexically after the first .PromptAsync(...) call IN THE SAME METHOD (catches both inline regression and helper-method shape, just like the sender fence). 5. Have its method-splitter regex regress to a shape that doesnt find RelayAsync (the self-test makes that silent-pass loud). Splitter difference vs sender: the receiver controller uses a primary constructor, so the splitter needs an atomic group on the modifier list to prevent the regex engine backtracking out of consuming "sealed" — otherwise "sealed class" folds into the return-type segment and the class header matches the method pattern. The negative lookahead alone is not enough without locking the modifier match. Refs #523. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(gateway): delete dead IAgentCommunicator infrastructure IAgentCommunicator + DefaultAgentCommunicator have zero production callers post-#530 — every named-to-sub and named-to-named call now flows through ISubAgentManager.SpawnAsync (for fire-and-forget) or IAgentExchangeService.ConverseAsync (for synchronous request/response). The receiver branch of cross-world federation moved to IConversationStore earlier in this PR, removing the last reason DefaultAgentCommunicator existed. Deleted: - src/gateway/BotNexus.Gateway.Contracts/Agents/IAgentCommunicator.cs - src/gateway/BotNexus.Gateway/Agents/DefaultAgentCommunicator.cs - tests/gateway/BotNexus.Gateway.Tests/DefaultAgentCommunicatorTests.cs (13 facts) - tests/gateway/BotNexus.Gateway.Tests/CrossAgentCallingTests.cs (6 facts) Updated: - GatewayServiceCollectionExtensions.cs:117 — DI registration removed - AssemblyLoadContextExtensionLoader.cs:44 — extension-loader entry removed - TypeForwards.cs:52 — type-forward removed - ISubAgentManager.cs:8 — stale XML cross-ref removed - BotNexus.Gateway.Abstractions/README.md — row removed Test-coverage audit confirmed all 19 deleted facts are mirrored by AgentExchangeServiceTests / AgentExchangeConversationTests EXCEPT the 2 timeout tests (CallCrossAgentAsync_WhenTargetTimesOut_*). AgentExchangeService uses caller-supplied cancellation tokens instead of the deleted PromptWithTimeoutAsync wrapper; the timeout-wrapper code path had no production callers, so the regression risk of dropping it is zero. Per AGENTS.md "no [Obsolete] attributes — delete dead code instead of deprecating it" and "do not net-delete tests; migrate them" (audit documents the migration). Refs #523. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(domain): delete SessionId.ForAgentConversation and ForCrossAgent These synthetic-encoding factories had no remaining callers after the sender (PR #548) and receiver (this PR) migrations routed both branches through IConversationStore with generic SessionId.Create() ids. Per AGENTS.md "delete dead code instead of deprecating it" — keeping them around as documentation-only would re-invite drift. KEPT: SessionId.IsAgentConversation predicate. SessionStoreBase.InferSessionType still reads it to bucket pre-migration sessions persisted with the legacy `::agent-agent::` encoding as SessionType.AgentAgent on read. Added test IsAgentConversation_OnLegacyEncodedId_StillReturnsTrue_ForBackCompatRead pinning that contract. Migrated tests (5 deleted, 1 added): - SessionIdTests.ForAgentConversation_UsesPinnedFormat — DELETED - SessionIdTests.ForAgentConversation_WhenUniqueIdIsEmpty_ShouldThrow — DELETED - SessionIdTests.ForCrossAgent_UsesPinnedFormat — DELETED - PrimitiveBoundaryTests.SessionId_ForAgentConversation_EmptyUniqueId_Throws — DELETED - PrimitiveBoundaryTests.SessionId_ForCrossAgent_WithSameSourceAndTarget_Works — DELETED + SessionIdTests.IsAgentConversation_OnLegacyEncodedId_StillReturnsTrue_ForBackCompatRead — NEW Also updated: - AgentExchangeService.cs:202-204 — stale "receiver will reuse our id" comment rewritten to reflect that each world owns its own ConversationId now. - docs/development/ddd-patterns.md:435 — SessionId factory list updated. Architecture fences in AgentExchangeConversationArchitectureTests and CrossWorldReceiverArchitectureTests are now belt-and-braces (compile-time prevents the call too) but kept as documentation/defensive markers. Refs #523. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(sessions): round-trip Session.Metadata in FileSessionStore The SessionMeta sidecar record omitted the Metadata dictionary, so Session.Metadata silently disappeared on every disk round-trip. Cross-world federation receiver depends on Session.Metadata [sourceWorldId] + [sourceAgentId] to validate RemoteSessionId ownership (see CrossWorldFederationController.OwnedByRequester). Without the round-trip, every cross-world relay returns 409 after the gateway restarts on a FileSessionStore-backed deployment. Add the Metadata field to SessionMeta + round-trip in Write/Load. Two new tests pin the contract: full key/value round-trip (with the codebase-standard ExtractMetadataString helper because System.Text.Json deserializes object? as JsonElement, consistent with SqliteSessionStore) plus empty-dict survival. Refs PR #549 critique sweep (bug-hunt BLOCKING #2). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(gateway): harden cross-world receiver per critique sweep Three independent issues surfaced by the PR #549 multi-model critique sweep — security/gpt-5.5, plan-vs-impl/claude-opus-4.6, and bug-hunt/gpt-5.3-codex — fixed together since they share the receiver controller and its test surface. 1. MetadataString silent failure (bug-hunt BLOCKING #1). The 'value as string' cast returned null when SqliteSessionStore / FileSessionStore deserialized object? metadata values as JsonElement (System.Text.Json default for object?). This drove every reused cross-world session past the 'value is null' guard into a one-character-fast-path-fail, then 409 — symptom looked like a stale session being rejected for missing ownership, but actually the controller couldn't see ownership it should have. Switch to the established codebase pattern (string | JsonElement(String) | _) already used by AgentConverseTool and PreCompactionMemoryFlusher. 2. Sealed-session silent reactivation (bug-hunt BLOCKING #5). ResolveSessionAsync reuse branch did not check Session.Status. A sealed session would be 'reused' for new turns even though the archive contract says sealed = terminal. Add an explicit GatewaySessionStatus.Sealed guard that returns 409 with a clear reason; pinned by RelayAsync_WhenRemoteSessionIdSupplied_ButSessionIsSealed_Returns409. 3. Agent-enumeration oracle (security/gpt-5.5 LOW). registry.Contains(targetAgentId) ran BEFORE inboundAuthService.TryAuthorize, so an unauthenticated probe against an unknown agent name returned 404 while a known agent name returned 401 — leaking the agent list to anyone with the relay endpoint. Reorder: authorise the peer first, return 401 uniformly for unauthenticated probes regardless of agent existence. Pinned by RelayAsync_WithInvalidApiKey_AndUnknownTargetAgent_Returns401_NotEnumerable404. Adds 7 critique-sweep tests including isolated coverage for each OwnedByRequester invariant guard (SessionType mismatch, sourceAgentId mismatch, missing conversation, no-bound-conversation) and the JsonElement-metadata reuse regression that proves fix (1). Refs PR #549 critique sweep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(architecture): replace vacuous receiver fence with helper-bypass-aware check The original per-method fence NoConversationIdMutation_AfterFirstPromptAsync_InRelayAsync was vacuously true: it split the file into method bodies, identified the RelayAsync body, then asserted '.ConversationId = does not appear after .PromptAsync('. Because the actual mutation lived in the helper ResolveSessionAsync (called BEFORE PromptAsync) and the prompt lived inline in RelayAsync, the per-method body of RelayAsync had neither the assignment nor a regression target — the check always passed even when the architecture broke (plan-vs-impl HIGH). Replace with a compound check (matching the actual invariant): 1. Inline check: within RelayAsync, every .ConversationId = assignment must lexically precede the first .PromptAsync(. 2. Helper-bypass check: for every helper method H in the file that (a) contains .ConversationId = AND (b) is invoked from RelayAsync AFTER the first .PromptAsync(, fail. This catches both regression shapes — inline mutation re-ordering AND someone extracting the eager pin into a late-stage helper that mirrors the original F-6 sub-agent eager-pin bug. Add three self-tests to prove the fence is not vacuous: - Vacuity guard: assert RelayAsync body is non-empty. - Synthetic violation: hand-crafted code with late mutation must FAIL. - Synthetic clean: hand-crafted code with early mutation must PASS. Refs PR #549 critique sweep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(sessions): document JsonElement metadata round-trip in SqliteSessionStore System.Text.Json deserializes Dictionary<string, object?> values as JsonElement (not their original primitive types). This affects both SqliteSessionStore AND FileSessionStore — readers must handle either shape (string OR JsonElement(String)). Add a docs-pin test that writes a Session.Metadata string value and asserts the reload is either a string or a JsonElement(String) — both shapes are acceptable, but downstream code must not assume one or the other (regression for the bug fixed in CrossWorldFederationController. MetadataString). Refs PR #549 critique sweep (bug-hunt BLOCKING #1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#563) * feat(domain): introduce SubAgentSpawnMode = Embody | Mirror DU (#562 step 1/5) Phase 5 / F-6 step 3, first staged commit of the migration to clarify sub-agent spawn intent at the type level. Today SubAgentSpawnRequest is a bag of optional fields where the "embody a role" path (Archetype + SystemPromptOverride + ModelOverride + ApiProviderOverride + ToolIds + Name) shares record-shape with the "mirror an existing named agent" path (TargetAgentId), and precedence is implicit in DefaultSubAgentManager. This commit introduces SubAgentSpawnMode as a closed discriminated union (abstract sealed record with exactly two derivations: Embody and Mirror) and threads an OPTIONAL Mode property onto SubAgentSpawnRequest so the new shape lands side-by-side with the old fields. No production code is rewired yet — that lands in commits 2 and 3. All old top-level fields on SubAgentSpawnRequest remain UNCHANGED; they are deleted and Mode becomes `required` in commit 5. Design decisions (locked via ask_user after rubber-duck design critique): 1. Embody is a strict pass-through of role + optional customisations. Role-derived defaults (per-role tool sets, prompts) are deferred to #467 — EmbodyCustomizations carries no derivation logic and every field is nullable with a `static readonly Default = new()` singleton for the common "no overrides" case. 2. Embody.Role is validated against a closed set of the 6 known SubAgentArchetype statics (Researcher, Coder, Planner, Reviewer, Writer, General). The smart-enum's open FromString registry stays intact for back-compat at other call sites, but the Embody record itself cannot smuggle unknown roles past construction. Validation runs from the [JsonConstructor] so deserialised payloads receive the same gate as direct construction. 3. Mirror is `record Mirror(AgentId TargetAgentId)` — strict, no metadata overrides. This is a deliberate behaviour change from today: the current code silently passes Name / ModelOverride / Archetype / ToolIds through to the mirrored child even though the intent is "mirror the target". Mirror clients that previously supplied any of these fields will be rejected by the tool layer in commit 2 with an explicit mode-mixing error. 4. The JSON discriminator property is "mode" with values "embody" and "mirror", declared via [JsonPolymorphic] + [JsonDerivedType]. No production boundary serialises SubAgentSpawnRequest today (ServiceBus uses InboundMessage envelopes; IInternalTrigger uses InternalTriggerRequest) — the annotations are defensive hygiene so future serialisation paths inherit the closed shape. 19 unit tests cover construction (Default singleton identity, all-null customisation pin, 6-role closed-set Theory, unknown-role rejection, null-arg rejection, Mirror construction), JSON round-trip both modes including full customisations, and rejection of missing / unknown discriminators. Note the missing-discriminator path raises NotSupportedException (not JsonException) — this is the contract STJ exposes for polymorphic abstract bases without a discriminator and is pinned by name. Out of scope for this commit: - SubAgentSpawnTool still constructs the old fields (commit 2). - DefaultSubAgentManager.SpawnAsync still pattern-matches on the old fields (commit 3). - Test callsites still use the old shape (commit 4). - Old fields and the architecture fence land in commit 5. Closes a piece of #562; remainder follows in the staged commit story. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(gateway): SubAgentSpawnTool builds Mode + rejects mode-mixing (#562 step 2/5) Phase 5 / F-6 step 3, second staged commit. The tool's agent-facing JSON shape is preserved exactly — the same flat object with task / name / model / apiProvider / tools / systemPrompt / archetype / targetAgentId — but the tool now translates that flat shape into the closed SubAgentSpawnMode union introduced in commit 1 and rejects mode-mixing. Behavioural changes: 1. The tool populates request.Mode in every call. * targetAgentId present + no embody fields → Mode = Mirror(AgentId.From(targetAgentId)). * targetAgentId present + ANY embody field → ArgumentException with the list of conflicting field names. The error message names every offending field in one shot so the agent can fix the call once, not iteratively. This is a deliberate behaviour change from today's silent pass-through — Mirror is strict. * targetAgentId absent + no embody fields → Mode = Embody(General, EmbodyCustomizations.Default). The Default singleton (same reference for every call with no overrides) keeps allocation at zero for the common case. * targetAgentId absent + any embody field → Mode = Embody(role, new EmbodyCustomizations { ... }) — only allocates when overrides are actually present. 2. The legacy top-level fields (Name / ModelOverride / etc. / TargetAgentId / Archetype) remain populated unchanged. This is deliberate — DefaultSubAgentManager.SpawnAsync is still on the old read path until commit 3. Both shapes are in sync per call so either reader produces identical behaviour during the migration window. Step 5 deletes the old shape atomically with making Mode `required` and removing the synced writes. 3. The unused private helper ReadArchetype was removed; the responsibility moved into the new BuildSpawnMode pipeline via ResolveArchetype (still using SubAgentArchetype.FromString for the open-registry back-compat path — Embody.Role's closed-set guard enforces the closed shape at the type boundary). Mode-mixing rejection design (per locked decision strict_reject_all): the JSON tool layer is the friendliest place to surface the conflict because the call hasn't yet entered the spawn pipeline. Throwing ArgumentException flows through the agent loop's ToolExecutor as an IsError=true tool result with the message body — the calling LLM sees "don't do that, here's why" and can self-correct in the next turn. 10 new unit tests: - Mode = Embody(General, Default) on the bare-minimum {task} call; the Default singleton identity is pinned via ShouldBeSameAs so the no-overrides path stays allocation-free. - Mode = Embody(role, Default) when only archetype is supplied (reviewer → Reviewer). - Mode = Embody(role, custom) with every embody field populated (Coder + name + model + apiProvider + tools + systemPrompt) — pins the full round-trip of every customisation field through the tool layer. - Mode = Mirror(AgentId) when only targetAgentId is supplied; the AgentId.Value is pinned so the Vogen wrapper isn't a no-op. - Mode-mixing rejection: a Theory over name / model / apiProvider / systemPrompt / archetype each pairs targetAgentId with one of the embody fields and pins the rejection message naming both fields. - Mode-mixing rejection: tools array (separate because the value shape differs from the string-Theory above). - Mode-mixing rejection: multiple conflicts in one call are all reported in a single error message (covers the "one error per call, not iterative fix" UX guarantee). Full Gateway.Tests suite: 1861 passed / 1 skipped / 0 failed / 0 warnings under TreatWarningsAsErrors. The legacy assertions on the old top-level fields (SpawnTool_SpawnsSubAgent_WithDefaults etc.) still pass — back-compat is intact. Refs #562. Next: commit 3 (DefaultSubAgentManager.SpawnAsync prefers Mode over the legacy fields). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(gateway): DefaultSubAgentManager.SpawnAsync pattern-matches on Mode (#562 step 3/5) Phase 5 / F-6 step 3 of 5. Routes the production spawn path (DefaultSubAgentManager.SpawnAsync) through the SubAgentSpawnMode discriminated union introduced in step 1 and constructed by SubAgentSpawnTool in step 2. What changed * DefaultSubAgentManager.SpawnAsync: the inline reads of request.Archetype / request.TargetAgentId / request.Name / request.ModelOverride / request.ApiProviderOverride / request.ToolIds / request.SystemPromptOverride are replaced by a single `switch (request.Mode)` that resolves 7 typed locals (archetype, baseDescriptor, childAgentId, name, modelOverride, apiProviderOverride, toolIds, systemPromptOverride) once. * The Embody arm uses the parent descriptor as the base and pins the archetype value into the child agent id role slot: `{parent}--subagent--{archetype}--{uniqueId}`. * The Mirror arm looks up the target descriptor via the agent registry (KeyNotFoundException if absent — fail-fast surfaces misconfiguration rather than silently degrading) and pins the target id into the role slot: `{parent}--subagent--{targetAgentId}--{uniqueId}`. Per the locked design (#562), Mirror is strict pass-through — no embody customisations leak through. * A `case null:` arm preserves the legacy field-population code path exactly so the ~12 existing test callsites that still set the top-level fields keep passing under back-compat through step 4. * A `default:` arm throws InvalidOperationException with the unknown subclass name — guards against silent acceptance if a third SubAgentSpawnMode subtype is added without updating SpawnAsync. * SubAgentInfo.Name and .Model now read from the resolved `name` / `modelOverride` locals (was request.Name / request.ModelOverride), ensuring the typed Mode arms drive what callers see post-spawn. Tests * tests/gateway/BotNexus.Gateway.Tests/Agents/SubAgentSpawnModeRoutingTests.cs NEW. 4 [Fact]s pin the Mode-driven SpawnAsync contract: - Embody clones parent + lands archetype in role slot. - Embody customisations propagate through to SubAgentInfo.Name + .Model (proves typed Mode actually drives the post-spawn surface). - Mirror uses target descriptor base + lands target id in role slot (locked-design pin: differentiates mirror from same-parent role spawn). - Mirror with unknown target throws KeyNotFoundException (fail-fast vs silent-degrade). Validation * Full Gateway.Tests suite: 1865 passed / 1 skipped / 0 failed / 0 warnings under TreatWarningsAsErrors=true (up from 1861 baseline; +4 new mode-routing pins). * Existing SubAgentTargetAgentIdTests + SubAgentToolTests (24 facts total) stay green under the case-null back-compat arm — no regressions introduced. Refs #562 * test(gateway): migrate legacy SubAgentSpawnRequest callsites to set Mode (#562 step 4/5) Phase 5 / F-6 step 4 of 5. Flips every test that calls `manager.SpawnAsync(...)` from setting the optional top-level fields (TargetAgentId / Archetype) to setting the typed `Mode = new Embody(...)` / `Mode = new Mirror(...)` discriminated union introduced in step 1 + threaded through the production paths in steps 2-3. Files migrated (10 callsites across 4 files): * Agents/SubAgentArchetypeIdentityTests.cs — both spawns: - Archetype = Reviewer → Mode = new Embody(Reviewer) - default → Mode = new Embody(General) * Agents/SubAgentEagerConversationPinTests.cs — all 3 spawns: - default → Mode = new Embody(General) * Agents/SubAgentKindTests.cs: - SpawnAsync_MirrorOfNamedAgent_*: TargetAgentId = "researcher-prime" → Mode = new Mirror(AgentId.From("researcher-prime")) - CreateSpawnRequest helper: default → Mode = new Embody(General) * Agents/SubAgentTargetAgentIdTests.cs — all 4 spawns: - TargetAgentId = "farnsworth" → Mode = new Mirror(AgentId.From("farnsworth")) - TargetAgentId = "ghost-agent" → Mode = new Mirror(AgentId.From("ghost-agent")) - no TargetAgentId → Mode = new Embody(General) - TargetAgentId = "farnsworth" (deny-list test) → Mode = new Mirror(...) Out of scope for this step: * Agents/SubAgentModelsTests.cs — pure domain-record reflection / equality tests, no SpawnAsync invocation. Its `RequiredProperties_AreMarkedRequired` test and the two `new SubAgentSpawnRequest` callsites will be updated in step 5 when the legacy fields are deleted and Mode becomes required. Why the SubAgentKindTests Mirror child id slot did NOT need test adjustment: ResolveChildAgentId(registry, spawned.SubAgentId) searches descriptors by substring against the per-spawn uniqueId, not against the role/target slot. So the slot-encoding change from `<parent>--subagent--<archetype>--<unique>` to `<parent>--subagent--<targetId>--<unique>` (locked design, #562) is transparent to the helper. No assertions on the role/target slot existed in the migrated test surface beyond what already lives in SubAgentSpawnModeRoutingTests.cs (added in step 3). Validation * Full Gateway.Tests suite: 1865 passed / 1 skipped / 0 failed / 0 warnings under TreatWarningsAsErrors=true. Same total as step 3 (1865) — net behaviour preserved by the typed Mode path; no silent regression in any of the 10 migrated tests. * Full solution build clean (0 W / 0 E). After this step the `case null:` back-compat arm in DefaultSubAgentManager.SpawnAsync has no remaining test callers — it will be deleted in step 5 along with the legacy fields themselves. Refs #562 * feat(gateway): delete legacy SubAgentSpawnRequest fields; Mode is required (#562 step 5/5) Final destructive step of the SubAgentSpawnMode discriminated-union work. The bag of optional top-level fields on SubAgentSpawnRequest has been deleted; every spawn must now pick a Mode (Embody | Mirror) at construction time. The mode-mixing class of bugs (callers populating an inconsistent subset of legacy fields that the runtime silently interpreted) is closed structurally — at the type, at the manager, and at the architecture fence. What changed - SubAgentSpawnRequest: delete Name, ModelOverride, ApiProviderOverride, ToolIds, SystemPromptOverride, Archetype, TargetAgentId. Mode is now `required SubAgentSpawnMode`. Kept: ParentAgentId, ParentSessionId, Task, MaxTurns, TimeoutSeconds, SpawnDepth, ParentToolDenyList, InheritedConversationId, Mode. - SubAgentSpawnTool: stop populating the deleted fields on the constructed request; build the typed Mode (Embody with customisations or Mirror with target) and hand it off. - DefaultSubAgentManager.SpawnAsync: delete the legacy `case null:` arm of the switch — Mode being required makes it structurally unreachable. The default arm is retained for the "unknown future subclass of SubAgentSpawnMode" guard. Deny-list check moved to run AFTER the switch so it reads the typed `toolIds` local resolved from the active arm rather than a now-deleted top-level field; check stays mode-agnostic so a future Mirror that produces a tool list is still policed. Tests - SubAgentModelsTests: required-props list now includes Mode; defaults test constructs with `Mode = new Embody(SubAgentArchetype.General)` and asserts the Mode round-trips; record-equality test uses an Embody mode in place of the deleted Name field. - Migrated remaining test factory helpers to set Mode: SubAgentToolInheritanceTests, DefaultSubAgentManagerActivityTests, DefaultSubAgentManagerTests (+ scaffold InterfaceBackedSubAgentManager reads name/model from Embody customisations), SubAgentCompletionWakeUpTests, SubAgentCompletionWakeDeliveryTests, SubAgentIntegrationTests, SubAgentSpawnDepthTests. - SubAgentToolTests: assertions rewritten from `captured.ModelOverride` / `.ToolIds` / `.SystemPromptOverride` / `.Archetype` to `captured.Mode` pattern-matched as Embody with the customisations and role verified. New architecture fence - tests/architecture/BotNexus.Architecture.Tests/SubAgentSpawnRequestShapeTests.cs: - SubAgentSpawnRequest_DoesNotExpose_LegacyFields — reflection-asserts each of the seven deleted properties is absent. - SubAgentSpawnRequest_Mode_IsRequired_AndTypedAsSubAgentSpawnMode — asserts Mode exists, is typed SubAgentSpawnMode, carries RequiredMemberAttribute, and is non-nullable per NullabilityInfoContext. - DefaultSubAgentManager_PatternMatchesOnMode_NotLegacyFields — source-scan asserts the manager pattern-matches on `request.Mode` and does not read any deleted legacy field via `request.<Name>`. - Fence_DetectsSyntheticLegacyFieldRead — vacuity guard: the legacy-field scan must catch a synthetic violation so a broken regex cannot silently neuter the fence. Validation - dotnet build BotNexus.slnx --nologo --tl:off → clean (0 warnings, 0 errors under TreatWarningsAsErrors). - dotnet test tests/gateway/BotNexus.Gateway.Tests → 1865 passed / 0 failed / 1 skipped (unchanged from steps 3+4). - dotnet test tests/architecture/BotNexus.Architecture.Tests → 53 passed / 0 failed (baseline 49 + 4 new shape pins). Refs #562 * fix(gateway): fold critique-sweep findings into SubAgentSpawnMode DU (#562) Multi-model critique sweep on the step-5 destructive-cleanup commit (36450e4) surfaced two HIGH and two MEDIUM findings. This commit folds the actionable subset into source + tests; deferred items are documented inline. Source fixes ------------ 1. Null-Mode runtime guard (bug-hunt HIGH). `required` is a compile-time guarantee only. A caller using `null!` or a JSON deserializer quirk could surface a runtime null Mode. The default switch arm would dereference `request.Mode.GetType()` and raise NullReferenceException — which propagates as HTTP 500 with an unhelpful stack trace. Added an `ArgumentNullException.ThrowIfNull` on `request.Mode` immediately after the existing request null-check so callers see a clean, named exception with the parameter name. 2. Mirror model fallback bug (bug-hunt MEDIUM). `SubAgentInfo.Model` was computed as `modelOverride ?? configuredDefaultModel ?? parentDescriptor.ModelId` on both Embody and Mirror paths. For Mirror with no modelOverride, this reported the *parent's* model id instead of the *mirrored target's* — silent metadata corruption that downstream telemetry, cost accounting, and observability would compute against the wrong model. Changed to `baseDescriptor.ModelId`, which covers both arms correctly (Embody: baseDescriptor = parentDescriptor; Mirror: baseDescriptor = mirrored target). Test fixes ---------- 3. Architecture fence regex tightening (plan-vs-impl HIGH). The step-5 fence on `DefaultSubAgentManager.cs` used literal `source.Contains(...)` to detect the Mode pattern-match. Mutations like `switch(request.Mode)` (no space) or `case Embody embody` would silently slip past, downgrading the fence to a no-op while leaving the suite green. Replaced with regexes: switch\s*\(\s*request\.Mode\s*\) \b(is|case)\s+Embody\b \b(is|case)\s+Mirror\b Tolerates IDE / formatter whitespace variations and both `case` and `is` pattern shapes. 4. Vacuity guard for the switch-detection clause (plan-vs-impl HIGH). The step-5 vacuity test (`Fence_DetectsSyntheticLegacyFieldRead`) only covered the legacy-field-read clause. Added a sibling `Fence_DetectsSyntheticModeSwitchShapes` that pins the regex behaviour against four synthetic mutations (no-space, padded parens, `is Embody`, `case Mirror`) and against a clean shape (must not false-positive). Without this, a regex regression could silently downgrade the fence while the legacy-field scan continues to pass. 5. JSON null-discriminator test pin (plan-vs-impl MEDIUM). Existing tests pinned missing discriminator (NotSupportedException) and unknown discriminator (JsonException). The `{"mode": null}` shape was not pinned. Added `Mode_JsonDeserialize_NullDiscriminator_Throws` asserting JsonException. A future STJ behaviour change for null discriminators would now be caught immediately. 6. Mirror-model assertion fold-in to routing test. `SpawnAsync_WithModeMirror_UsesTargetDescriptor_AndChildIdUsesTargetSlot` already pinned the *registered descriptor's* ModelId but not the *returned* `info.Model`. Added the assertion so a regression to the parent's model id (the bug fixed in #2 above) fails this test directly rather than slipping through. 7. Null-Mode test pin (bug-hunt HIGH). Added `SpawnAsync_WithNullMode_ThrowsArgumentNullException` to pin the new runtime guard at the manager boundary. Deferred items -------------- * Security HIGH (Mirror authz gap): a caller with `spawn_subagent` can name any registered targetAgentId and inherit that agent's tools / model / system-prompt without authz. Authorization is out of scope for this plan (operator owns auth; only agent-provider auth is in scope). Operators control Mirror target registration — treat the registry as a trust boundary. * Plan-vs-impl HIGH #1: archetype context-table mismatch (my notes said "Tester", code says "Writer"). Code is correct; documentation fix only, not a behavioural defect. * Plan-vs-impl LOW #6/#7: out-of-scope SubAgentInfo contract changes for Mirror's `Archetype = General` label. * Plan-vs-impl MEDIUM #4: Mirror inherits target's tools that may be on the parent's deny-list (intentional pass-through; deny-list is mode-agnostic by design — Mirror is strict pass-through, target registration is the trust boundary). Validation ---------- * Build: 0 warnings / 0 errors under TreatWarningsAsErrors. * Gateway.Tests: 1867 passed / 1 skipped / 0 failed (+2 from step 5). * Architecture.Tests: 54 passed / 0 failed (+1 vacuity guard from step 5's 53). Refs #562. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) (#565) Caller-initiated cancellation of a cross-world relay used to permanently seal the session via the catch-all `catch (Exception)` block that fired on every OCE — including the caller's own. The sealed-session 409 guard in `ResolveSessionAsync` then turned a transient client timeout or a closed browser tab into a permanently broken sender-retry contract. Insert a guarded re-throw BEFORE each catch-all so caller-initiated cancellation rethrows without sealing: catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // #553: caller-initiated cancellation must NOT seal the session. throw; } catch (Exception ex) { // existing seal-on-error logic, now only sees inner-token cancels // and genuine failures. } The `when` filter is the discriminator: inner-token cancellations (downstream HTTP-client timeouts linked into the supervisor token) MUST still flow through to the catch-all and seal, because they are genuine downstream failures, not callers pulling out. Patched sites: - CrossWorldFederationController.ExecuteRelayAsync - AgentExchangeService.ConverseAsync (local agent-agent path) - AgentExchangeService.ConverseAsync (cross-world relay-out path) Acceptance criteria (#553) pinned by behavioural tests: - AC #1 — cancelled relay throws OCE, session stays Active: RelayAsync_WhenCallerCancelsDuringPromptAsync_RethrowsOce_AndDoesNotSealSession ConverseAsync_LocalPath_WhenCallerCancelsDuringPromptAsync_RethrowsOce_AndDoesNotSealSession - AC #2 — no error metadata recorded on cancellation: same tests assert Metadata.ContainsKey("error").ShouldBeFalse() - AC #3 — retry with same RemoteSessionId succeeds (exercises the real RelayAsync + ResolveSessionAsync reuse path including the OwnedByRequester / 5-invariant ownership check from PR #549): RelayAsync_AfterCallerCancellation_SessionIsReusableByRetryWithSameRemoteSessionId Vacuity guard pinning the `when` filter (an inner CTS unrelated to the caller cancels, OCE bound to inner; caller's IsCancellationRequested is false ⇒ filter false ⇒ falls through to catch-all ⇒ seals. Removing the filter would catch the inner OCE and rethrow without sealing, failing this test): RelayAsync_WhenInnerTokenCancels_NotCallerToken_StillSealsSession ConverseAsync_LocalPath_WhenInnerTokenCancels_NotCallerToken_StillSealsSession Architecture fitness fence (CancelNoSealArchitectureTests): - Per-catch fence on both production files: every `catch (Exception)` body containing `GatewaySessionStatus.Sealed` must be preceded by the required OCE-rethrow clause within the prior 1500 chars. - Counter-fence on both production files: each file must contain ≥N Sealed-writing catch-alls, so a future refactor to a helper (e.g. SealSessionAsync(session)) is forced to either keep the literal or update the fence regex — otherwise the per-catch fence would silently pass with zero candidates. - Vacuity guards: synthetic catch-all without OCE guard, synthetic bare OCE catch without `when` filter, both must violate. - False-positive guards: canonical post-#553 shape and canonical shape with a brace-bearing comment inside the rethrow body must NOT violate. Body regex permits ONE level of nested braces so explanatory comments like `// see Foo {...}` don't push future authors to delete them. - Detector specificity: positive-shape detection and non-sealing catch (e.g. ClearActiveSessionAsync diagnostic catches) correctly ignored. Trade-offs and follow-ups: - ActiveSessionId is intentionally NOT cleared on caller cancellation. A fast retry sees the conversation as still in-flight rather than briefly flipping idle → active in the portal. Staleness is bounded by SessionCleanupService TTL (default 24h, 5m sweep) and ActiveSessionId is documented as a diagnostic, not a correctness contract. - Catch-all SaveAsync calls pass CancellationToken.None on all three sites, so caller-token OCE cannot fire during the seal write — the race that would leave Status mutated in-memory but unflushed is structurally avoided. - ClearActiveSessionAsync diagnostic catches (controller, service) are deliberately outside the fence's scope — they swallow without sealing. - Cancelled turn's user-input is persisted before PromptAsync; on retry the user message would be appended again. This is a real but bounded duplicate-message UX/transcript concern (NOT a security issue per the security review) and is filed as a deferred follow-up — solving it requires per-turn idempotency keys, which is out of scope for the cancel-no-seal fix. Multi-model critique sweep was run before opening: - security-review (gpt-5.5): no vulnerabilities; DoS bounded by TTL sweeper, authz boundary preserved. - plan-vs-impl review (claude-opus-4.7): all 3 ACs + `when` filter + rethrow + 3-site coverage + fence non-vacuity pinned. Adopted two SHOULD-CONSIDER findings (regex loosening for brace-bearing comments, counter-fence on Sealed catch count). - bug-hunt (gpt-5.3-codex): no blocking issues. Confirmed lock scope is released by `await using`, token identity is the method parameter, vacuity guard truly pins the discriminator. Closes #553. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR Parallelism Map — 2026-06-02✅ Can merge in parallel (independent)
⏩ Must merge sequentially (dependencies)
|
| PR | Conflicting With | Shared Files |
|---|---|---|
| #712 | #715 | No shared files (Skills: BlazorClient/Gateway; #715: GatewayHub/tests only) |
Notes:
- PR refactor(signalr): wire GatewayHub onto IInboundMessageOrchestrator and drop obsolete JoinSession/LeaveSession (#714) #715 is in MERGEABLE/CLEAN state — safe to merge now
- PR feat(skills): add Skills Explorer portal section with gateway API #712 is in UNKNOWN mergeable state (likely needs rebase check) — touches MainLayout.razor and GatewayRestClient which may have evolved
- Both PRs are independently mergeable with no file conflicts between them
- After refactor(signalr): wire GatewayHub onto IInboundMessageOrchestrator and drop obsolete JoinSession/LeaveSession (#714) #715 merges: file PR2 (TUI cleanup), PR5 (InternalChannelAdapter), PR6 (ChannelBindingPolicy) for [Channels] Phase 10 — channels own their conversation binding strategy (W-5 umbrella) #676 arc
PR Parallelism Map — 2026-06-03✅ Can merge in parallel (independent)
⏩ Must merge sequentially (dependencies)
|
| PR | Conflicting With | Shared Files |
|---|---|---|
| #751 | #727 | Both touch tests/integration/BotNexus.Integration.E2E.Tests/ — PortalPage.cs, SlashCommandTests.cs, SidebarNavigationTests.cs are modified by #727 while #751 modifies other files in the same directory. Review for merge order: merge #727 first or coordinate. |
Notes
- All three PRs are
UNSTABLE(CI not passing). fix(e2e): audit and align all E2E tests to current UI design #751 and test(e2e): mobile chat coverage - scroll and error bar regression tests (#722 #723) #727 areMERGEABLE; feat(ui): remove mic button - audio recording not yet supported #741 isUNKNOWN(likely needs CI re-run). - feat(ui): remove mic button - audio recording not yet supported #741 is safe to merge independently once CI passes — only touches
ChatPanel.razor,app.css, and a new test file. - PR test(e2e): mobile chat coverage - scroll and error bar regression tests (#722 #723) #727 adds regression tests for bugs Mobile: chatScroll.js not loaded in index.html - auto-scroll completely broken #722 and Mobile: blazor-error-ui bar is undismissable and shows no error detail - unusable on mobile #723 — the actual production fixes (adding
<script src="js/chatScroll.js">to mobileindex.htmland a dismiss button toblazor-error-ui) are not yet in any PR. Recommend filing or checking for a companion fix PR.
PR Parallelism Map — 2026-06-03✅ Can merge in parallel (independent)
⏩ Must merge sequentially (dependencies / conflicts)
|
| PR | Conflicting With | Shared Files |
|---|---|---|
| #751 | #727 | PageObjects/PortalPage.cs, SlashCommandTests.cs, SidebarNavigationTests.cs |
| #727 | #751 | same |
Recommendation: Merge #741 immediately (no conflicts). For #751 and #727 — pick one to merge first; the other needs a rebase before it can land.
PR Parallelism Map — 2026-06-04Open PRs
✅ Can merge in parallel (independent)
⏩ Must merge sequentiallyNo sequential dependencies detected — only 1 open PR.
|
|
` |
PR Parallelism Map -- 2026-06-05Open PRs
Can merge in parallel (no file overlap)
Recommended merge order#826 first (smallest, GatewayHost only), then #821 (requires #820 already merged -- done), then #827. Any order is safe. |
PR Parallelism Map -- 2026-06-05 (updated)Open PRs
Can merge in parallel (no file overlap)
Merge order recommendationAny order is safe across all 9 PRs. No stacking dependencies. |
PR Parallelism Map -- 2026-06-05 (updated)Open PRs
Can merge in parallel (no file overlap)
Merge order recommendationAny order is safe across all 11 PRs. No stacking dependencies. |
PR Parallelism Map — 2026-06-05 (morning)Open PRs
Merge order recommendationsStandalone (any order):
Sequential (stacking pairs):
Dependency-free pairs for this cycle:
File overlap map
|
…on (#1063) NormalizeFuzzyMatchWithMap iterated char-by-char and called .Normalize(NormalizationForm.FormKC) on individual characters. When a character was a high surrogate (first half of an emoji surrogate pair), .Normalize() threw ArgumentException because lone surrogates are not valid Unicode code points. Fix: detect surrogate pairs and emit them together without normalization. Lone surrogates (invalid Unicode) are replaced with U+FFFD to prevent crashes on corrupt files. This was the #1 failure mode across all file tools (113 occurrences, 77% of all edit errors in session DB). Closes #1061 Co-authored-by: BotNexus Test <botnexus@test.local>
PR Parallelism Map -- 2026-06-2010 open PRs. Every PR touches a fully disjoint file set -- zero shared files across the entire queue. All 10 are file-independent and can merge in any order. The only sequencing note is a CI-unblock ordering (not a file conflict): #1543/#1544/#1546 currently show ✅ Can merge in parallel (independent -- no shared files, no issue-level dependency)
⏩ Must merge sequentially (dependencies)
|
PR Parallelism Map — 2026-06-2110 open PRs, all ✅ Can merge in parallel (independent — all 10)
⏩ Must merge sequentiallyNone. No PR's linked issue is a
|
PR Parallelism Map — 2026-06-2310 open PRs. File-set analysis (live ✅ Can merge in parallel (independent) — all 10
⏩ Must merge sequentially (dependencies)None. Disjoint file sets and no open-PR↔open-PR dependency edge.
|
PR Parallelism Map — 2026-06-24Open PRs: 1 — the merge queue drained hard since yesterday (Jon batch-merged ~17 PRs across two drains: ✅ Can merge in parallel (independent)
⏩ Must merge sequentially (dependencies)None — single open PR.
|
IActivityStream/ActivityStreamto broadcast all system eventsIActivityStreamabstraction +IActivitySubscriptionin CoreActivityEventmodel (MessageReceived, ResponseSent, DeltaSent, AgentProcessing, AgentCompleted, Error)ActivityStreamimplementation using bounded channels with DropOldest policyActivityStreaminServiceCollectionExtensionsGET /api/sessions— lists all sessions with metadata (key, agent, channel, message count, timestamps)GET /api/sessions/{key}— returns full session history with role/content/timestamp/tool infoGET /api/channels— lists channels with name, displayName, running status, streaming supportGET /api/agents— lists configured agents with model, temperature, tokens, etc.GatewayWebSocketHandlerto support{"type":"subscribe"}messageMessageReceivedevents when dispatching messagesErrorevents when processing failsResponseSentandDeltaSentevents{"type":"activity","event":{...}}to subscribed clientsBotNexus.WebUIproject with static HTML/CSS/JSindex.html— SPA layout with sidebar (sessions, channels, agents, activity monitor) + chat areastyles.css— dark theme chat UI with responsive designapp.js— WebSocket connection, REST API calls, session browsing, real-time chat, activity feedBotNexus.WebUIto solution fileActivityStream(publish, subscribe, broadcast, dispose, concurrency)Original prompt