Part of #1313
Summary
Complete and host the server-side chat-client transport listener so that a REMOTE Phantom.Workspaces user-computer-profile can receive an AgentDefinition on the chat-client channel, construct a CopilotSdkChatClient (or any other executor IChatClient) via AgentFactory, and serve it back to the SOURCE via ChatClientOverTransport. Register the listener in production so this path is reachable.
This is foundational for the docs/design/remote-chat-client-session.md topology (local router + local persistence + remote SDK chat client). Without this, no source AgentChat can obtain a remote CopilotSdkChatClient.
Dependencies
Root Cause / Current State
features/Phantom.Workspaces.Transport/Chat/ChatClientTransportListener.cs:12 — the constructor takes a pre-built IChatClient chatClient.
ChatClientTransportListener.cs:20-29 — OnChannelOpenAsync delegates every chat-client channel to that single fixed instance. It never reads an AgentDefinition from the wire payload; it cannot per-channel construct an executor client.
features/Phantom.Workspaces.Llm.Core/AgentFactory.cs:210, 224, 301 — CreateChatClient / CreateChatClientAsync, with the github-copilot provider dispatch at :278 / :353, and the Copilot-SDK client builder starting at CreateGitHubCopilotClient :814+. These are the intended factories the listener should invoke.
features/Phantom.Workspaces.Transport/ReverseHttp/ReverseExecutionDispatcher.cs — the reverse-http transport server dispatch. It does not register a chat-client listener today (see docs/design/unified-transport-production-cutover.md), so even a correctly-implemented listener is not hosted in production.
Net effect: today there is no wire path where the remote instance receives an AgentDefinition and builds a CopilotSdkChatClient per turn; #1313's scenario cannot be reached.
Affected Files
features/Phantom.Workspaces.Transport/Chat/ChatClientTransportListener.cs
features/Phantom.Workspaces.Transport/ReverseHttp/ReverseExecutionDispatcher.cs
- Transport host/registry wiring on the server side (whatever registers listeners in production per
docs/design/unified-transport-production-cutover.md).
- (No test-only touches here; test harness lands in sibling sub-items.)
Design / Fix
- Extend
ChatClientTransportListener (or introduce a companion listener) so OnChannelOpenAsync reads an AgentDefinition from the chat-client open payload and builds the executor IChatClient per channel via AgentFactory.CreateChatClient(AgentDefinition) / CreateChatClientAsync(...) (dispatching through github-copilot at AgentFactory.cs:278 / :353 → Copilot SDK client builder at :814+).
- The constructed client's lifetime is scoped to the channel; dispose on channel close.
- Register the listener on the transport server /
ReverseExecutionDispatcher following the pattern described in docs/design/unified-transport-production-cutover.md. The registration must be reachable in production, not test-only.
- Preserve the existing pre-built
IChatClient path (used by current tests) as an overload / fallback where the payload omits AgentDefinition, so this change is additive and non-breaking.
Expected Tests
| Test |
Asserts |
ChatClientTransportListener_ReceivesAgentDefinition_BuildsChatClientViaAgentFactory |
Given a chat-client channel opened with a wire-carried AgentDefinition (provider github-copilot), the listener invokes AgentFactory.CreateChatClientAsync (verified via spy/fake factory) and services subsequent chat requests through the resulting IChatClient. |
ChatClientTransportListener_PerChannelClientLifetime_DisposesOnChannelClose |
The IChatClient built for a channel is disposed when the channel closes (verified via disposable spy). |
ReverseExecutionDispatcher_RegistersChatClientListener_ServesChatClientChannels |
With production wiring in place, a chat-client channel open request is dispatched to ChatClientTransportListener (verified against HubRelayHarness or equivalent loopback). |
ChatClientTransportListener_LegacyPreBuiltChatClientPath_StillWorks |
Existing pre-built-IChatClient usage continues to serve channels when the payload omits AgentDefinition (regression guard). |
Part of #1313
Summary
Complete and host the server-side chat-client transport listener so that a REMOTE Phantom.Workspaces user-computer-profile can receive an
AgentDefinitionon thechat-clientchannel, construct aCopilotSdkChatClient(or any other executorIChatClient) viaAgentFactory, and serve it back to the SOURCE viaChatClientOverTransport. Register the listener in production so this path is reachable.This is foundational for the
docs/design/remote-chat-client-session.mdtopology (local router + local persistence + remote SDK chat client). Without this, no sourceAgentChatcan obtain a remoteCopilotSdkChatClient.Dependencies
Root Cause / Current State
features/Phantom.Workspaces.Transport/Chat/ChatClientTransportListener.cs:12— the constructor takes a pre-builtIChatClient chatClient.ChatClientTransportListener.cs:20-29—OnChannelOpenAsyncdelegates everychat-clientchannel to that single fixed instance. It never reads anAgentDefinitionfrom the wire payload; it cannot per-channel construct an executor client.features/Phantom.Workspaces.Llm.Core/AgentFactory.cs:210, 224, 301—CreateChatClient/CreateChatClientAsync, with thegithub-copilotprovider dispatch at:278/:353, and the Copilot-SDK client builder starting atCreateGitHubCopilotClient:814+. These are the intended factories the listener should invoke.features/Phantom.Workspaces.Transport/ReverseHttp/ReverseExecutionDispatcher.cs— the reverse-http transport server dispatch. It does not register a chat-client listener today (seedocs/design/unified-transport-production-cutover.md), so even a correctly-implemented listener is not hosted in production.Net effect: today there is no wire path where the remote instance receives an
AgentDefinitionand builds aCopilotSdkChatClientper turn; #1313's scenario cannot be reached.Affected Files
features/Phantom.Workspaces.Transport/Chat/ChatClientTransportListener.csfeatures/Phantom.Workspaces.Transport/ReverseHttp/ReverseExecutionDispatcher.csdocs/design/unified-transport-production-cutover.md).Design / Fix
ChatClientTransportListener(or introduce a companion listener) soOnChannelOpenAsyncreads anAgentDefinitionfrom thechat-clientopen payload and builds the executorIChatClientper channel viaAgentFactory.CreateChatClient(AgentDefinition)/CreateChatClientAsync(...)(dispatching throughgithub-copilotatAgentFactory.cs:278/:353→ Copilot SDK client builder at:814+).ReverseExecutionDispatcherfollowing the pattern described indocs/design/unified-transport-production-cutover.md. The registration must be reachable in production, not test-only.IChatClientpath (used by current tests) as an overload / fallback where the payload omitsAgentDefinition, so this change is additive and non-breaking.Expected Tests
ChatClientTransportListener_ReceivesAgentDefinition_BuildsChatClientViaAgentFactorychat-clientchannel opened with a wire-carriedAgentDefinition(providergithub-copilot), the listener invokesAgentFactory.CreateChatClientAsync(verified via spy/fake factory) and services subsequent chat requests through the resultingIChatClient.ChatClientTransportListener_PerChannelClientLifetime_DisposesOnChannelCloseIChatClientbuilt for a channel is disposed when the channel closes (verified via disposable spy).ReverseExecutionDispatcher_RegistersChatClientListener_ServesChatClientChannelschat-clientchannel open request is dispatched toChatClientTransportListener(verified againstHubRelayHarnessor equivalent loopback).ChatClientTransportListener_LegacyPreBuiltChatClientPath_StillWorksIChatClientusage continues to serve channels when the payload omitsAgentDefinition(regression guard).