Repository navigation
Sub-agent chat view renders text that belongs to the parent agent #1109
Description
Part of #1102.
Related: #1110 (shared root cause — the inverse-facing symptom; fixed together by the single-path rewrite below). See also sub-agent siblings #1108, #1111, #1112.
Summary
When a Copilot SDK sub-agent is running, streamed text/tool output that originated from the parent agent's turn can be rendered inside the sub-agent's chat transcript. The sub-agent view shows content it never produced, corrupting the transcript and misleading the user about what each agent actually did. #1110 is the inverse face of the same defect (sub-agent output rendered in the parent view).
The underlying flaw is structural, not a single mis-set flag: sub-agent turn-event routing is implemented as TWO divergent code paths selected purely by factory != null (a registry path and a factory path) that resolve targets, register child sinks, and marshal updates in different, non-equivalent ways. Any content that the active path fails to attribute to a child silently falls back to the parent sink. As the owner put it: "Why do we need two separate paths? … There should be only one 'path'. If-then-else logic leads to logic errors like this." This issue is therefore fixed by collapsing the two paths into a single, deterministic routing path — and, because the branch keyed on factory-presence is the structural source of the bug, by enforcing that RunningAgentChatFactory is always present at construction time so the null case becomes impossible (owner directive: "RunningAgentChatFactory must always be provided at construction time. Let's enforce that."). See Design / Fix.
Root Cause
The two divergent paths
CopilotSubAgentRouter (Phantom.Workspaces.Llm.Core\CopilotSubAgentRouter.cs) branches on this.factory in four places, giving two entirely different routing mechanisms:
Sub-agent start (HandleSubAgentStartedAsync, lines 162–195):
if (this.factory is not null && this.subAgentTable is not null) // FACTORY path (line 172)
{
await this.HandleSubAgentStartedWithFactoryAsync(agentId).ConfigureAwait(false);
}
else if (this.registry is { } reg) // REGISTRY path (line 176)
{
var childSink = await reg.GetOrCreateAsync(agentId, subDef, parentToolCallId ?? string.Empty)...;
...
}Per-update push (PushUpdate / GetTarget, lines 246–270):
private void PushUpdate(string? agentId, ChatResponseUpdate update)
{
if (!string.IsNullOrEmpty(agentId) && this.factory is not null) // FACTORY path (line 248)
{
if (this.factoryReceivers.TryGetValue(agentId, out var entry))
entry.Receiver.Push(update);
else
this.logger?.LogWarning("Received sub-agent event for unknown sub-agent ID '{AgentId}'; ignoring.", agentId); // DROPPED
return;
}
this.GetTarget(agentId).Push(update); // REGISTRY path
}
private ISubAgentChat GetTarget(string? agentId) // lines 267–270
=> string.IsNullOrEmpty(agentId)
? this // null agentId -> ROOT/parent sink
: this.registry?.TryGet(agentId) ?? this; // unknown id -> ROOT/parent sinkSub-agent result (HandleSubAgentResultAsync, lines 197–241) branches on factory again (line 207 factory-receiver completion vs. lines 233–240 registry.TryGet).
The two paths are not equivalent:
| Concern | Registry path (factory == null) |
Factory path (factory != null) |
|---|---|---|
| Target resolution | registry.TryGet(agentId); null/unknown → parent (GetTarget, 267–270) |
factoryReceivers[agentId]; unknown → dropped + logged (248–259) |
| Child-sink creation | registry.GetOrCreateAsync (181), flows the parent foreground scheduler — the #913 fix that makes child output render live |
factory.CreateAsync + subAgentTable.Add (272–286) |
| Registration timing | resolved before content flows | async fire-and-forget window: factoryReceivers[agentId] is set only after the await chain (284–285); UI insertion is Task.Factory.StartNew(..., foregroundScheduler) (AgentChat.cs:1061) |
Untagged / null-agentId content |
→ parent sink | → parent sink (via GetTarget(null)) |
Attribution itself is a per-content property that is only stamped when the SDK supplies a non-empty AgentId (CopilotSdkStreamAdapter.Tag, 222–231), read back by GetParentToolCallId (195–201); the SDK sometimes omits AgentId (TryGetSubAgentId, 214–220), so genuine child content can arrive untagged and fall to root.
RunningAgentChatFactory is an optional, set-later dependency — the ambiguity the branch exploits
RunningAgentChatFactory is modeled as nullable and optionally-set everywhere it flows, which is exactly what lets a chat exist in two states (factory-present vs. factory-absent) and lets routing branch on that state:
AgentServices.RunningAgentChatFactoryisIRunningAgentChatFactory?(Phantom.Workspaces.Llm.Interfaces\AgentServices.cs:30, aninitproperty on arecord).AgentChat.InitializeAsyncreads it defensively asthis.request.AgentServices?.RunningAgentChatFactory as IRunningAgentChatFactory(AgentChat.cs:261) and passes the possibly-null value into the client.CopilotSdkChatClientstores it asIRunningAgentChatFactory? runningAgentChatFactory(CopilotSdkChatClient.cs:60) and sets it after construction viaSetSubAgentDependencies(IRunningAgentChatFactory? factory, ISubAgentTable? table)(CopilotSdkChatClient.cs:438–442).- That nullable value becomes the router's
IRunningAgentChatFactory? factory(CopilotSubAgentRouter.cs:32, defaulted tonullin the ctor at:54, forwarded fromCopilotSdkChatClient.cs:535). - The restore path tolerates its absence with a warn-and-skip branch (
AgentChat.RestoreSubAgentsAsync,AgentChat.cs:1099–1109).
Because the type is nullable end-to-end, both "GUI leaves it null" (pre-#1036) and "factory self-injected" (post-#1036) are representable, and routing keys off exactly that difference.
The regression that flipped production onto the factory path
Culprit commit: e5c559b5 — "Fix #1036: self-inject factory so reopened sessions restore sub-agents" (2026-07-15). Known-good: 826827ab (Fix #1033). The per-content tag/fallback logic itself is old (since #622/#674) and unchanged. What regressed is which path production takes.
AgentChatFactory.WithSelfAsFactory now stamps a non-null RunningAgentChatFactory into every chat's services (AgentChatFactory.cs:243–246, applied at 78/154/213):
private AgentServices WithSelfAsFactory(AgentServices baseServices)
=> baseServices.RunningAgentChatFactory is null
? baseServices with { RunningAgentChatFactory = this }
: baseServices;That value flows AgentServices.RunningAgentChatFactory → AgentChat.InitializeAsync (AgentChat.cs:261) → CopilotSdkChatClient.SetSubAgentDependencies (CopilotSdkChatClient.cs:438–442) → the router's this.factory (CopilotSdkChatClient.cs:532–537).
- Before
e5c559b5: the GUI never setRunningAgentChatFactory(confirmed by the Bug: sub-agent output never appears live in the HTML chat view — registry-path sub-agent AgentChat gets no foreground scheduler; WebView auto-flush timer binds to a dead thread-pool dispatcher #913 commit message; the GUI still constructsnew AgentServices()with no factory atApp.axaml.cs:272andMainWindowViewModel.cs:188), sofactory == null→ the registry path, which Bug: sub-agent output never appears live in the HTML chat view — registry-path sub-agent AgentChat gets no foreground scheduler; WebView auto-flush timer binds to a dead thread-pool dispatcher #913 had just fixed to flow the parent's foreground scheduler so child output renders live. This was the historically-correct behavior. - After
e5c559b5:factory != nullfor every chat → the router takes the factory path, whose async registration window and parent/drop fallbacks reintroduce the parent↔child misrouting reported here (and in Sub-agent output appears in the parent chat view instead of the sub-agent view #1110).
git bisect between 826827ab and HEAD converges on e5c559b5.
Affected Files
| File | Contribution |
|---|---|
Phantom.Workspaces.Llm.Core\CopilotSubAgentRouter.cs (162–195) |
HandleSubAgentStartedAsync branches on factory — two creation mechanisms |
Phantom.Workspaces.Llm.Core\CopilotSubAgentRouter.cs (246–270) |
PushUpdate/GetTarget — factory-receiver vs. registry resolution; parent-sink / drop fallbacks |
Phantom.Workspaces.Llm.Core\CopilotSubAgentRouter.cs (197–241) |
HandleSubAgentResultAsync branches on factory |
Phantom.Workspaces.Llm.Core\CopilotSubAgentRouter.cs (32, 54, 272–286) |
Nullable factory field / defaulted-null ctor param; HandleSubAgentStartedWithFactoryAsync async fire-and-forget receiver-registration window |
Phantom.Workspaces.Llm.Interfaces\AgentServices.cs (30) |
RunningAgentChatFactory typed IRunningAgentChatFactory? — the nullable/optional dependency to be made mandatory |
Phantom.Workspaces.Llm.Core\CopilotSdkChatClient.cs (60, 438–442, 532–537) |
Nullable runningAgentChatFactory field; SetSubAgentDependencies(IRunningAgentChatFactory? …) set-later injection; forwards nullable factory to router |
Phantom.Workspaces.Llm.Core\AgentChat.cs (261, 1061, 1099–1109) |
Reads AgentServices?.RunningAgentChatFactory defensively; foreground-scheduler UI insertion for sub-agents (#913); restore path tolerates a null factory |
Phantom.Workspaces.Llm.Core\CopilotSdkStreamAdapter.cs (195–231) |
Tag/GetParentToolCallId/TryGetSubAgentId — attribution is a conditionally-present per-content tag |
Phantom.Workspaces.Llm.Core\AgentChatFactory.cs (78/154/213/243–246) |
WithSelfAsFactory stamps a non-null factory into every chat (the #1036 trigger; the sanctioned single injection point) |
Phantom.Workspaces.Agent.Gui\App.axaml.cs (272), Phantom.Workspaces.Agent.Gui\...\MainWindowViewModel.cs (188) |
GUI constructs new AgentChatFactory(store, new AgentServices(), scheduler) — the construction entry points whose chats must now always carry a factory |
Design / Fix — a single unified routing path over an always-present factory
Delete the factory != null branch entirely. There must be exactly one registration model, one deterministic target-resolution, and one scheduler-marshaling flow, applied identically for every chat. This is made structurally sound by enforcing that RunningAgentChatFactory is always provided at construction time (see the enforcement sub-section below): with the factory always present, the deleted branch is not merely unified — its null case can no longer occur.
1. One child-sink registry, keyed by child agent-session-id / agentId. Collapse factoryReceivers and registry into a single Dictionary<string, ISubAgentChat> childSinks. The factory receiver is adapted to the same ISubAgentChat sink abstraction so there is only one sink type.
2. Synchronous registration — eliminate the race. On a sub-agent-started signal, insert a buffering sink into childSinks[agentId] synchronously, before any await, then do the async construction and swap in the real receiver, flushing buffered updates in order. Content can never arrive before its sink exists, and never lands on the parent during the construction window:
private async Task HandleSubAgentStartedAsync(FunctionCallContent start)
{
var agentId = start.CallId;
if (string.IsNullOrEmpty(agentId)) return;
// Register synchronously so no update for this child can ever race ahead of its sink.
var sink = new BufferingSubAgentChat();
this.childSinks[agentId] = sink;
// Construct on the parent's foreground scheduler (the #913 correctness flow) and register
// in the sub-agent table so reopened sessions restore (#1036). The factory is ALWAYS present.
var child = await this.CreateChildChatAsync(agentId, start).ConfigureAwait(false);
sink.AttachAndFlush(child, this.foregroundScheduler); // in-order flush on the foreground scheduler
}3. One deterministic target resolution — never silently fall back to the parent for child content.
private void PushUpdate(string? agentId, ChatResponseUpdate update)
=> this.GetTarget(agentId).Push(update);
private ISubAgentChat GetTarget(string? agentId)
{
if (string.IsNullOrEmpty(agentId))
return this; // genuine root/parent content only
// A child agentId is ALWAYS registered synchronously at sub-agent-started, so a known child
// always has a (possibly buffering) sink. A present-but-unregistered agentId is held in a
// late-arrival buffer keyed by agentId — never misattributed to the parent.
return this.childSinks.TryGetValue(agentId, out var sink)
? sink
: this.HoldForLateRegistration(agentId); // buffer; flush when the child registers
}4. One completion path. HandleSubAgentResultAsync resolves the same childSinks[agentId] sink and calls Complete()/Fail() — no factory branch.
Enforce RunningAgentChatFactory as a mandatory construction-time dependency
Per the owner directive, RunningAgentChatFactory must always be provided at construction time — never null, never optionally set later. This complements the single-path fix: because routing no longer branches on factory presence, the factory should simply always exist, eliminating the null/non-null ambiguity that caused the regression.
Make the dependency non-nullable and guard it at construction (throw if missing).
-
CopilotSubAgentRouter— change the field and ctor parameter from nullable/defaulted to a required, non-nullable dependency, guarded:private readonly IRunningAgentChatFactory factory; // was IRunningAgentChatFactory? (line 32) private readonly ISubAgentTable subAgentTable; // was ISubAgentTable? (line 33) internal CopilotSubAgentRouter( ChannelWriter<ChatResponseUpdate> rootWriter, IRunningAgentChatFactory factory, // no `= null` default (was line 54) ISubAgentTable subAgentTable, ILogger? logger = null) { ArgumentNullException.ThrowIfNull(factory); ArgumentNullException.ThrowIfNull(subAgentTable); ... }
The
ISubAgentChatRegistry? registryfield is removed with the registry path. -
CopilotSdkChatClient— make the field non-nullable and the injector reject null:private IRunningAgentChatFactory runningAgentChatFactory = default!; // was nullable (line 60) internal void SetSubAgentDependencies(IRunningAgentChatFactory factory, ISubAgentTable table) // was `?` (line 438) { ArgumentNullException.ThrowIfNull(factory); ArgumentNullException.ThrowIfNull(table); this.runningAgentChatFactory = factory; this.subAgentTable = table; }
-
AgentChat.InitializeAsync— guard at the single call site so no chat can be constructed without a factory (replacing the defensiveasread at line 261):var factory = (this.request.AgentServices?.RunningAgentChatFactory as IRunningAgentChatFactory) ?? throw new InvalidOperationException( "RunningAgentChatFactory must be supplied at construction time; AgentServices.RunningAgentChatFactory was null."); copilotChatClient.SetSubAgentDependencies(factory, this);
The warn-and-skip null branch in
RestoreSubAgentsAsync(AgentChat.cs:1099–1109) is removed — the factory can no longer be null. -
AgentServices.RunningAgentChatFactorystays typedIRunningAgentChatFactory?at the record level (it is legitimately absent for non-agent service bags and cannot reference a not-yet-constructed factory), but every chat-producing path must populate it. Enforcement lives at the chat-construction guard above, not on the record.
The sanctioned single injection point is AgentChatFactory.WithSelfAsFactory — retained, not replaced. There is a genuine chicken-and-egg: the GUI constructs new AgentServices() and hands it to new AgentChatFactory(...) (App.axaml.cs:272, MainWindowViewModel.cs:188) before the factory object exists, so the GUI cannot itself put the factory into AgentServices. WithSelfAsFactory is the one place that closes the loop, so it is kept — but simplified to inject unconditionally (the pre-#1036 "GUI leaves it null" scenario is being removed, so there is nothing to preserve):
// Every chat this factory creates carries the factory so restore (#1036) and live sub-agent
// creation always work. This is the single, mandatory injection point.
private AgentServices WithSelfAsFactory(AgentServices baseServices)
=> baseServices with { RunningAgentChatFactory = this };Because it is applied on every create path (AgentChatFactory.cs:78/154/213), and sub-agent child chats are themselves created through the factory (factory.CreateAsync at CopilotSubAgentRouter.cs:276, and the restore path at AgentChat.cs:1094+), child chats obtain the factory at construction too — the guarantee is transitive.
Construction sites that must supply the factory (verified):
| Site | file:line | How it satisfies the guarantee |
|---|---|---|
| GUI app startup | Phantom.Workspaces.Agent.Gui\App.axaml.cs:272 |
Constructs the AgentChatFactory; every chat it creates is stamped by WithSelfAsFactory |
| GUI main window | ...\MainWindowViewModel.cs:188 |
Same — constructs the AgentChatFactory used for all sessions |
| Live sub-agent child creation | CopilotSubAgentRouter.cs:276 (factory.CreateAsync) |
Child chat created via the factory → stamped |
| Restore / reopen | AgentChat.cs:1094+ (RestoreSubAgentsAsync → factory.GetOrCreateAsync) |
Restored child created via the factory → stamped; null-tolerant branch removed |
Tests that build a real AgentChat/AgentServices |
e.g. ChatOutputByokEndToEndWebViewTests.cs:157 (new AgentServices() into new AgentChatFactory(...)) |
Must go through the factory (or explicitly set RunningAgentChatFactory) so the construction guard passes |
5. Routing becomes factory-neutral and factory-guaranteed → removes the regression trigger. No routing decision reads this.factory, and the factory is always present, so the #1036 WithSelfAsFactory change is both routing-neutral and now the enforced norm: stamping a non-null RunningAgentChatFactory no longer changes which path runs (there is only one), and can never be absent.
Net result: child content always renders in the child view; parent content always renders in the parent view; untagged/not-yet-registered content is buffered and resolved deterministically instead of leaking; the async registration window is closed; and a chat can never be constructed in a factory-less state.
Expected Tests
New/updated tests in the existing routing/factory test classes. The existing AssistantMessageDeltaEvent_NullAgentId_WrittenToRootStream (CopilotSdkChatClientSubAgentRoutingTests, lines 95–109) encodes the wrong "null → root fallback" rule that this defect exploits and is REPLACED by RootDelta_WithNoActiveSubAgent_WrittenToRootStream (keeps the legitimate genuine-root case) plus the child-attribution and enforcement tests below.
| Test Name | Class | What It Verifies |
|---|---|---|
SubAgentDelta_ForRegisteredChild_RoutedToChild_NeverRootStream |
CopilotSdkChatClientSubAgentRoutingTests |
Child content always reaches the child sink; the root stream stays empty |
ParentTurnDelta_WhileSubAgentActive_NeverRoutedToChildSink |
CopilotSdkChatClientSubAgentRoutingTests |
Genuine parent-turn content never appears in any child sink while a sub-agent is active |
RootDelta_WithNoActiveSubAgent_WrittenToRootStream |
CopilotSdkChatClientSubAgentRoutingTests |
Genuine root content (no owning child) stays in the root stream (replaces the old null→root test) |
SubAgentDelta_BeforeReceiverCreated_BufferedThenFlushedToChild_NotParent |
CopilotSdkChatClientSubAgentFactoryTests |
Child content emitted during the construction window is buffered and flushed to the child, never the parent |
SubAgentStarted_RegistersChildSinkSynchronously_BeforeAsyncCreate |
CopilotSdkChatClientSubAgentFactoryTests |
The child sink is registered synchronously before the async create, so no update can race ahead of its receiver |
UnknownSubAgentId_HeldForLateRegistration_NotMisattributedToParent |
CopilotSdkChatClientSubAgentFactoryTests |
A present-but-unregistered agentId is held/buffered (not dropped, not pushed to parent) |
SubAgentRouting_WithFactoryAlwaysPresent_BehavesIdentically |
CopilotSdkChatClientSubAgentFactoryTests |
Routing is deterministic and identical across turns now that a factory is always present (the factory != null branch is gone) |
RestoredSubAgentOnSessionReopen_RoutesChildContentToChild |
CopilotSdkChatClientSubAgentFactoryTests |
A sub-agent restored on a reopened session (#1036) still routes its content to the child view |
SubAgentRouter_ConstructedWithoutFactory_Throws |
CopilotSubAgentRouterTests |
Constructing CopilotSubAgentRouter with a null factory throws ArgumentNullException (mandatory dependency) |
SetSubAgentDependencies_NullFactory_Throws |
CopilotSdkChatClientSubAgentFactoryTests |
CopilotSdkChatClient.SetSubAgentDependencies(null, …) throws — the factory can never be set-later-as-null |
AgentChatInitialize_WithoutRunningAgentChatFactory_Throws |
AgentChatSubAgentRegistryTests |
Building an AgentChat whose AgentServices.RunningAgentChatFactory is null throws at construction (guard) |
AgentChatFactory_CreatedChat_AlwaysCarriesRunningAgentChatFactory |
AgentChatSubAgentRegistryTests |
Every chat produced by AgentChatFactory (WithSelfAsFactory) has a non-null RunningAgentChatFactory, including restored/reopened sessions |
SubAgentChildChat_ConstructedWithFactory |
CopilotSdkChatClientSubAgentFactoryTests |
A live sub-agent child chat is constructed with a non-null factory (transitive guarantee) |
Considered / Background — the dual-path design and optional factory are being removed
The following describes the superseded design, retained only as context:
- Attribution by optional per-content tag.
CopilotSdkStreamAdapter.Tag(222–231) stampedParentToolCallIdPropertyNameonly whenAgentIdwas non-empty;GetParentToolCallId(195–201) read it back, returningnullfor untagged content, which routing treated as root-origin. The unified design keeps synchronous per-child registration so a missing tag can no longer send child content to root. - Registry path (Bug: sub-agent output never appears live in the HTML chat view — registry-path sub-agent AgentChat gets no foreground scheduler; WebView auto-flush timer binds to a dead thread-pool dispatcher #913). Introduced the foreground-scheduler flow so child output renders live in the sub-agent view; this is the correctness behavior the unified path preserves.
- Factory path (Wire CopilotSdkChatClient to handle SubAgentStarted using IRunningAgentChatFactory and ISubAgentTable #674) + self-injection (Sub-agents not restored on session reopen: RunningAgentChatFactory never injected into AgentServices #1036). Added live child creation via
IRunningAgentChatFactory+ISubAgentTableand sub-agent restoration on reopened sessions; the unified path preserves restoration while removing the divergent routing/async-registration window. - Optional / set-later factory.
RunningAgentChatFactorywas nullable end-to-end (AgentServices.cs:30,CopilotSdkChatClient.cs:60/438,CopilotSubAgentRouter.cs:32/54) and set after construction, allowing a chat to exist without a factory (the pre-Sub-agents not restored on session reopen: RunningAgentChatFactory never injected into AgentServices #1036 GUI case). This is the ambiguity the branch exploited; it is removed by enforcing a mandatory construction-time factory.
The considered narrow fixes — (a) scoping WithSelfAsFactory to restore-only, or (b) bringing the factory path to parity with the registry path, or (c) leaving the factory optional and only unifying routing — are rejected in favor of a single path over a mandatory factory per the owner directive, because keeping two paths (or a nullable factory) is the structural source of these logic errors.
Activity
JoshuaRowePhantom commentedon Jul 22, 2026
Verification result
Status: ✅ Verified
Commits checked:
Outstanding gaps
None — all criteria satisfied.
Notes
The two divergent sub-agent routing paths (registry vs factory) are collapsed into a single deterministic path built on
IRunningAgentChatFactory+ISubAgentTable. Both dependencies are non-nullable and mandatory:CopilotSubAgentRouterctor throwsArgumentNullExceptionon null factory or table (SubAgentRouter_ConstructedWithoutFactory_Throws,_WithoutSubAgentTable_Throws);CopilotSdkChatClient.SetSubAgentDependenciesthrows on null;AgentChat.InitializeAsyncthrowsInvalidOperationExceptionwhen aCopilotSdkChatClientis hosted without aRunningAgentChatFactory;RestoreSubAgentsAsyncthrows (instead of warn-and-skip) when a factory is missing and persisted children exist;AgentChatFactory.WithSelfAsFactoryinjects unconditionally. Routing uses a singlechildSinksdictionary of per-agentIdChildRoutingEntry: entries are inserted synchronously (under lock) BEFORE anyawaiton either lifecycle-start OR routed child update, so no update can race the child sink into existence; updates arriving beforefactory.CreateAsynccompletes are buffered and flushed in order viaChildRoutingEntry.Attach(also races safely with completion/failure). Untagged deltas (agentIdempty) go to the parent root; non-emptyagentIddeltas ALWAYS go to aChildRoutingEntryand never fall back to the parent. Registry-only path deleted, along with its 362-line test file - unified path is covered instead by the rewrittenCopilotSubAgentRouterTests+CopilotSdkChatClientSubAgentFactoryTests(buffered-then-flushed, null-guard throws, unknown-id-buffered,RootDelta_WithNoActiveSubAgent_WrittenToRootStream,SubAgentDelta_ForRegisteredChild_RoutedToChild_NeverParent, restored session routing, etc.). Foreground-scheduler correctness from #909/#913 is preserved: childAgentChatinstances are still constructed viaAgentChatFactory, which threads the per-chat foregroundTaskScheduler; the router marshals through the child'sICopilotSubAgentReceiver(which owns the foreground marshalling), and root writes still flow through theChannelWriterseeded on the parent's foreground scheduler. RanMainWindowIntegrationTests,AgentFactoryByokTests,CopilotSubAgentRouterTests,CopilotSdkChatClientSubAgentFactoryTests,AgentChatHostedSubAgentTests,AgentChatResumeTests,AgentViewModelSubAgentNavigationTests,AgentViewModelEditorTreeTests,AgentChatEditorControlTests,AgentViewModelNavigationTests,AgentChatSlashCommandRegistrationTests(fast) - 296 passed, 0 failed - plus 10 consecutive stability passes onCopilotSubAgentRouterTests,CopilotSdkChatClientSubAgentFactoryTests,AgentChatHostedSubAgentTests,AgentViewModelSubAgentNavigationTests,AgentViewModelNavigationTests,AgentViewModelEditorTreeTests,AgentChatEditorControlTests.Tests
Fix #1109: unify sub-agent routing on the factory/table path and make…
Fix #1110: sub-agent output never leaks into the parent transcript
Fix test fallout from #1107 and #1109
JoshuaRowePhantom commentedon Jul 22, 2026
Checked in
Commit: fa758e0
Branch fast-forwarded: features → fa758e0
Fix #1180: route manifest launchpad through IRunningAgentChatTable
Fix #1461: add workspace New Agent shortcut with manifest selection tab