Skip to content

Remote agent sessions don't restore chat history; add a "Restoring history ..." HTML indicator and surface restore exceptions #1304

Description

@JoshuaRowePhantom

Note

This bug is part of the broader "remote agent sessions" design being written. This specific bug covers: loading a remote session's chat history from the remote data store over the dev tunnel (for display), plus a Restoring history ... HTML indicator and surfacing restore exceptions. Where the agent process itself runs (locally on the remote profile machine) is covered by the parent design.

Summary

When we open an existing agent session whose associated user-computer-profile is a different (remote) computer reachable over a dev tunnel, the chat output pane comes up empty — no history is restored.

The correct behavior is:

  • The agent should start / run locally on that remote profile computer (i.e. on the session's own host profile machine, not on the machine that opened it).
  • The chat history for the session should be loaded from the remote data store over the dev tunnel (the same dev-tunnel data-access channel we already use for entities and for WebClientAgentPersistenceStore) and displayed in the local client.

Today neither happens for history: the local AgentChat used as the display facade has its persistence store replaced with NullAgentPersistenceStore, so it loads zero messages and the pane renders empty. My earlier framing on this bug ("no transport RPC fetches remote history, treat it as a fundamentally missing feature") was wrong: the history already lives in the remote store and is already reachable over the dev-tunnel data-access path — it just isn't being read for display.

In addition:

  • Add a "Restoring history ..." HTML indicator that stays visible until history is fully restored.
  • Surface any exception raised during restore into a visible error banner instead of silently showing nothing.

Root Cause

Verified in the features submodule.

1. Local history restore path (works)

  • Phantom.Workspaces.Llm.Core/AgentChat.cs InitializeAsync reads persisted messages via ConfiguredStore.ReadMessagesAsync(...) (~L382), pushes them with LoadInitialHistory(persistedMessages) (~L415), and completes historyPopulated.TrySetResult() (~L420). HistoryPopulated is exposed at ~L511.
  • UI side: Phantom.Workspaces.Agent.Gui/Controls/AgentChatOutputControl.axaml.cs OnBrowserReady awaits vm.HistoryPopulated (~L303), then builds ChatOutputHtmlModel(vm.History, ...) (~L322). The model prepends history chunks via ChatOutputHtmlModels.cs LoadHistoryChunksAsync (~L1968-2084) into container id chat-history-container (ChatOutputHtmlRenderer.HistoryContainerId ~L33; shell markup at chat-output-shell.html ~L506).

2. Why remote sessions never restore history (primary bug)

Opening an existing agent session is routed through Phantom.Workspaces/ViewModels/OpenAgentSessionShortcutHandler.cs (~L310-337). When the session's host-profile-entity-id (ReadHostProfileEntityId ~L455) does not match the local user-computer-profile, the handler calls CreateTrustedAgentChatAsync (~L434-453), which dispatches to Phantom.Workspaces.Llm.Core/Transport/TransportTrustedExecutor.CreateAgentChatAsync (~L43).

That method sets:

var services = baseServices with
{
    ChatClientOverride = chatClient,
    AgentPersistenceStoreOverride = NullAgentPersistenceStore.Instance,   // ~L56
};

Phantom.Workspaces.Llm.Core/NullAgentPersistenceStore.cs:

  • ReadMessagesAsync returns Array.Empty<ChatMessage>() (~L32-35)
  • RestoreAsync returns null (~L26-29)
  • Class doc-comment (~L6-11) explicitly states the remote side owns authoritative history and the local side must never prepend stored history to outbound messages.

So AgentChat.InitializeAsync loads zero messages for remote sessions, historyPopulated completes immediately, and the chat renders empty. The "don't prepend stored history to outbound" rule is correct and must be preserved — but nothing currently reads the remote history for display.

3. The remote history IS already reachable over the dev tunnel

The history the local display should show is already persisted on the remote instance and served over the same dev-tunnel data-access channel we use for other data:

  • Authoritative store on the remote instance: Phantom.Workspaces.Data.MongoDB/MongoDbAgentPersistenceStore.cs.
  • HTTP endpoint exposing it: Phantom.Workspaces.Web.Server/AgentPersistenceEndpointRouteBuilderExtensions.cs (routes under /agent/persistence/...).
  • Dev-tunnel-aware client: Phantom.Workspaces.Data.Web.Client/WebClientAgentPersistenceStore.cs (~L10-46 constructs an HttpClient against the dev-tunnel endpoint, sends the X-Tunnel-Authorization header, and calls /agent/persistence/store, /agent/persistence/read-messages, etc.).
  • Reconnect wrapper used by the app: Phantom.Workspaces/Services/DevTunnel/ReconnectingWebAgentPersistenceStore.cs.

The transport-based execution path (TransportTrustedExecutor) does not use this store — it forces NullAgentPersistenceStore — even though we already have a working dev-tunnel-backed IAgentPersistenceStore implementation.

4. Exceptions during restore are swallowed

  • Phantom.Workspaces.Agent.Gui/Controls/ChatOutputHtmlModels.cs LoadHistoryChunksAsync catches only OperationCanceledException (~L2075); other exceptions fault the HistoryLoaded task silently.
  • AgentChatOutputControl.axaml.cs swallows exceptions from await vm.HistoryPopulated (~L305-309) and inside CompleteWhenLoadedAsync (~L351-354) with empty catch blocks — no UI feedback.
  • AgentChat.RunSessionInitAsync (~L435-445) does emit an ErrorContent "Failed to load session: {ex}" — but only for the session-init phase with the real store. NullAgentPersistenceStore never throws, so remote-restore failures produce no error at all today, and any future remote-fetch failure would similarly need an explicit surface.

5. No existing loading/status affordance

  • chat-output-shell.html has only <div id="chat-history-container"></div> (~L506). No status / spinner / restoring element.
  • There is a .chat-error CSS class used by ErrorContent (ChatOutputHtmlRenderer.cs ~L532) that can be reused for the new error banner.
  • The render model has a private historyLoading field (ChatOutputHtmlModels.cs ~L1605, set true at ~L1650, false at ~L2063) that is never surfaced as bindable state. HistoryLoaded task at ~L1613.
  • AgentViewModel.cs exposes HistoryPopulated (~L288) — the natural place to add IsRestoringHistory / RestoreError.

Affected Files

File Lines Role
features\Phantom.Workspaces.Llm.Core\AgentChat.cs 185-465, 511 InitializeAsync loads history via configured store; exposes HistoryPopulated.
features\Phantom.Workspaces.Llm.Core\NullAgentPersistenceStore.cs 6-35 Substituted for remote sessions; returns no messages. Its "no outbound prepend" intent is correct and must be preserved.
features\Phantom.Workspaces.Llm.Core\Transport\TransportTrustedExecutor.cs 43-65 Forces AgentPersistenceStoreOverride = NullAgentPersistenceStore.Instance; must instead provide a read-only, dev-tunnel-backed store for display.
features\Phantom.Workspaces\ViewModels\OpenAgentSessionShortcutHandler.cs 310-337, 434-453 Detects remote host-profile and routes through TransportTrustedExecutor.
features\Phantom.Workspaces.Data.Web.Client\WebClientAgentPersistenceStore.cs 10-60+ Existing dev-tunnel IAgentPersistenceStore (HTTP + X-Tunnel-Authorization). Candidate for the display-side restore path.
features\Phantom.Workspaces\Services\DevTunnel\ReconnectingWebAgentPersistenceStore.cs 7-49 Reconnect wrapper around the web-client store used by the app.
features\Phantom.Workspaces\Services\AgentPersistenceStoreCache.cs 8-38 Caches per-RepositorySource IAgentPersistenceStore instances.
features\Phantom.Workspaces.Agent.Gui\Controls\AgentChatOutputControl.axaml.cs 303-322, 351-354 Awaits HistoryPopulated; empty catches swallow exceptions.
features\Phantom.Workspaces.Agent.Gui\Controls\ChatOutputHtmlModels.cs 1605, 1613, 1650, 1968-2084 LoadHistoryChunksAsync; internal historyLoading never surfaced; only catches OperationCanceledException.
features\Phantom.Workspaces.Agent.Gui\Controls\ChatOutputHtmlRenderer.cs 33, 532 HistoryContainerId constant; .chat-error CSS class.
features\Phantom.Workspaces.Agent.Gui\Controls\chat-output-shell.html 506 Shell markup; needs status + error containers.
features\Phantom.Workspaces.Agent.Gui\ViewModels\AgentViewModel.cs 288 Exposes HistoryPopulated; needs IsRestoringHistory + RestoreError.

Design / Fix

  1. Load remote history from the remote store over the dev tunnel (for display). In TransportTrustedExecutor.CreateAgentChatAsync (~L43-65), instead of setting AgentPersistenceStoreOverride = NullAgentPersistenceStore.Instance, provide an IAgentPersistenceStore that reads the session's history from the remote data store over the existing dev-tunnel data-access channel (i.e. the same WebClientAgentPersistenceStore / ReconnectingWebAgentPersistenceStore path already used elsewhere in the app; the remote server exposes it via AgentPersistenceEndpointRouteBuilderExtensions on top of MongoDbAgentPersistenceStore).

    Constraint (preserve the NullAgentPersistenceStore intent): the local display facade must remain read-only for outbound messages. That is: ReadMessagesAsync / RestoreAsync should hit the remote store over the dev tunnel and return real history for display, but StoreAsync / AddSubAgentLinkAsync on the display facade must NOT write — the remote agent owns authoritative writes, so we must not prepend locally-stored history to outbound messages. A thin read-only wrapper over WebClientAgentPersistenceStore (delegating reads, no-op on writes) satisfies this.

    The dev-tunnel endpoint / access token to use is the one associated with the session's host user-computer-profile (resolved the same way TransportTrustedExecutor already resolves the transport target via ExecutionTargetResolver).

  2. "Restoring history ..." HTML indicator. In chat-output-shell.html add sibling elements near ~L506:

    <div id="chat-history-status"></div>
    <div id="chat-history-error"></div>

    Add HistoryStatusId / HistoryErrorId constants in ChatOutputHtmlRenderer.cs near ~L33. Drive them from ChatOutputHtmlModels.cs: show Restoring history ... when history load begins (~L1650, historyLoading = true) and clear when the load completes (~L2063). Surface the state on AgentViewModel as IsRestoringHistory (near ~L288) so the indicator also covers the async remote fetch window — the indicator must remain until history is fully restored (both the remote-store fetch and the local chunked render), not just for the local chunk render.

  3. Surface restore exceptions. Wrap LoadHistoryChunksAsync (~L1972+) and the remote fetch in catch (Exception ex) that writes ex.ToString() into the new chat-history-error element (styled with .chat-error from ChatOutputHtmlRenderer.cs ~L532). Replace the empty catch blocks in AgentChatOutputControl.axaml.cs (~L305-309, ~L351-354) with error-banner reporting. Add a RestoreError string on AgentViewModel.

  4. Full-restore semantics. IsRestoringHistory reflects the union of the async phases (remote fetch + local chunked render) and only transitions to false after both complete.

Considered / Background

  • Adding a brand-new "transport RPC that fetches remote history" (the framing in the earlier version of this bug) is unnecessary: the remote store is already exposed over HTTP via the dev tunnel through WebClientAgentPersistenceStore and the AgentPersistenceEndpointRouteBuilderExtensions routes. The fix reuses that channel rather than inventing a new transport message.
  • Keeping NullAgentPersistenceStore intact and adding a separate "restore history over dev tunnel" call that populates AgentChat.History post-InitializeAsync is an alternative. Wiring the read-only web-client store as AgentPersistenceStoreOverride is preferred because it lets InitializeAsync populate History and complete HistoryPopulated through its existing code path.

Expected Tests

Existing test files to align with (read before implementing):

  • features\Phantom.Workspaces.Agent.Gui.Tests\AgentChatOutputControlTests.cs
  • features\Phantom.Workspaces.Agent.Gui.Tests\ChatOutputHtmlRendererTests.cs
  • features\Phantom.Workspaces.Agent.Gui.Tests\ChatOutputHtmlModelTests.cs
  • features\Phantom.Workspaces.Agent.Gui.Tests\AgentViewModelTests.cs
  • features\Phantom.Workspaces.Llm.Core.Tests\AgentChatPersistenceTests.cs
  • features\Phantom.Workspaces.Llm.Core.Tests\TransportTrustedExecutorTests.cs (if present; otherwise add a new file next to the other Phantom.Workspaces.Llm.Core.Tests transport tests)
  • features\Phantom.Workspaces.Data.Web.Client.Tests\WebClientAgentPersistenceStoreTests.cs

Naming convention: Subject_Scenario_ExpectedOutcome (PascalCase).

Test Name Class What It Verifies
ChatOutputShellHtml_ContainsHistoryStatusAndErrorContainers AgentChatOutputControlTests The shell HTML includes chat-history-status and chat-history-error sibling elements next to chat-history-container.
ChatOutput_WhileRestoringHistory_ShowsRestoringIndicator ChatOutputHtmlModelTests The Restoring history ... indicator is shown while restore is in progress and hidden once history is fully restored.
LoadHistoryChunks_WhenRestoreThrows_EmitsErrorBanner ChatOutputHtmlModelTests An exception during LoadHistoryChunksAsync is rendered into the chat-history-error element (styled .chat-error) instead of being swallowed.
TransportTrustedExecutor_RemoteSession_UsesDevTunnelBackedPersistenceStoreForRead TransportTrustedExecutorTests For a remote target, AgentPersistenceStoreOverride is set to a dev-tunnel-backed read-only store (delegating reads to WebClientAgentPersistenceStore) rather than NullAgentPersistenceStore.
TransportTrustedExecutor_RemoteSession_LoadsHistoryFromRemoteStoreOverDevTunnel TransportTrustedExecutorTests Opening a remote session invokes ReadMessagesAsync against the dev-tunnel IAgentPersistenceStore for the session id, and AgentChat.History is populated with the returned messages before HistoryPopulated completes.
TransportTrustedExecutor_RemoteSession_DisplayStoreDoesNotWriteOnStore TransportTrustedExecutorTests The display-side store used for the local AgentChat no-ops on StoreAsync / AddSubAgentLinkAsync, preserving the NullAgentPersistenceStore "no outbound prepend / no local write" intent.
AgentViewModel_IsRestoringHistory_TrueUntilHistoryFullyRestored AgentViewModelTests IsRestoringHistory is true from restore start and only transitions to false after both the remote fetch and the local chunked render complete.
AgentChatOutputControl_WhenHistoryPopulatedThrows_ReportsRestoreError AgentChatOutputControlTests Exceptions from await vm.HistoryPopulated are surfaced as RestoreError / an error banner instead of being swallowed by empty catch blocks.

Exact method/class names above should be reconciled against the existing test files before implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdiagnosedRoot cause identified

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions