first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+110
View File
@@ -0,0 +1,110 @@
# REMOTE-1478: Follow-up session attach for ambient agent conversations
## Context
Ambient cloud-agent views currently use the session-sharing viewer stack as a one-session transport. The initial cloud-mode view is created as a deferred shared-session viewer, and `create_cloud_mode_view` subscribes to `AmbientAgentViewModelEvent::SessionReady` to call `viewer::TerminalManager::connect_to_session` once in `app/src/terminal/view/ambient_agent/mod.rs (28-72)`.
That one-shot shape is encoded in `viewer::TerminalManager`. `NetworkState::PendingJoin` owns `prompt_type` and `channel_event_proxy`; `connect_session` consumes those values, creates a `Network`, wires subscriptions, and transitions to `NetworkState::Active`. After that, `connect_to_session` returns `false` for `Active`, so the same manager cannot attach to a second session ID in `app/src/terminal/shared_session/viewer/terminal_manager.rs (73-292)`.
When the shared session ends, the viewer path treats the view as permanently finished. `Network::process_websocket_message` handles `DownstreamMessage::SessionEnded` by calling `close_without_reconnection` and emitting `NetworkEvent::SessionEnded` in `app/src/terminal/shared_session/viewer/network.rs (597-600)`. The terminal manager then calls `shared_session_ended`, which cancels in-progress conversations, unregisters from the shared-session manager, calls `TerminalView::on_session_share_ended`, sets `SharedSessionStatus::FinishedViewer`, and clears the write-to-PTY sender in `app/src/terminal/shared_session/viewer/terminal_manager.rs (635-684)` and `app/src/terminal/shared_session/viewer/terminal_manager.rs (1397-1425)`.
`TerminalView::on_session_share_ended` is a UI teardown routine, not a resumable boundary. It may insert the conversation-ended tombstone, clears `shared_session`, unregisters remote peers, flips viewer input to selectable/read-only, and updates pane sharing state in `app/src/terminal/view/shared_session/view_impl.rs (683-735)`.
The viewer network is scoped to a single session ID and websocket endpoint. Same-session reconnect is handled internally by `Network::reconnect_websocket`, but this work does not need new same-session reconnect behavior. Follow-up executions should join a new session with a fresh `Network` and `InitPayload.last_received_event_no: None`, while preserving the existing `TerminalView`, `TerminalModel`, ambient view model, and AI history.
The first join path loads a session snapshot through `EventLoop::new`. It decodes the `scrollback` from `JoinedSuccessfully` and calls `TerminalModel::load_shared_session_scrollback` before processing ordered terminal events in `app/src/terminal/shared_session/viewer/event_loop.rs (68-131)`. That model method restores serialized blocks into the current blocklist but has no explicit follow-up mode or duplicate-block policy in `app/src/terminal/model/terminal_model.rs (1443-1455)` and `app/src/terminal/model/blocks.rs (729-757)`.
The session-sharing server already supports joining arbitrary session IDs as fresh sessions. A fresh viewer gets `JoinedSuccessfully` with that session's scrollback, active prompt, source type, and latest event number from `/Users/zachbai/dev/session-sharing-server/server/src/sessions/manager/join.rs (190-259)` and `/Users/zachbai/dev/session-sharing-server/server/src/sessions/manager/join.rs (546-641)`. The missing client-side contract is how much of a follow-up session's scrollback is new versus rehydrated from the previous VM.
## Implemented changes
The implementation introduces a fresh-session attach path for ambient follow-up executions. This is not a same-session reconnect feature. A follow-up always means:
- the previous session has ended;
- the caller has a new `SessionId`;
- the ambient agent run ID is unchanged;
- the existing ambient terminal view/model should remain the user-visible conversation;
- a new viewer `Network` should join the new session;
- the new session's contents should append to the existing blocklist.
### Rework viewer manager connection ownership
The one-shot `NetworkState` shape was replaced with state that keeps reusable viewer resources outside the per-session network:
- `NetworkResources { prompt_type, channel_event_proxy }`
- `current_network: Arc<FairMutex<Option<ModelHandle<Network>>>>`
- `NetworkState::Idle | Connecting | Active(ModelHandle<Network>)`
`new_internal` now stores `prompt_type` and `channel_event_proxy` on the manager instead of hiding them inside a pending-join state. Initial viewers call the same internal `connect_session` helper used by deferred cloud-mode viewers with `SharedSessionInitialLoadMode::ReplaceFromSessionScrollback`.
The public attach API is intentionally narrow:
- `connect_to_session(session_id, ctx)` for the initial deferred attach;
- `attach_followup_session(session_id, ctx)` for ambient follow-up attaches.
For follow-up attaches, `attach_followup_session`:
- close/drop the old `current_network` if present;
- install a fresh write-to-PTY channel on the terminal model;
- set shared-session status back to `ViewPending`;
- create a new `Network` for the new session ID;
- wire inbound network events for that specific network;
- update `current_network`.
### Avoid per-session outbound subscription leaks
Outbound subscriptions that previously captured a concrete `network` handle now register once and route through `current_network`: view events, LLM preference changes, input mode changes, selected conversation changes, auto-approve changes, CLI agent input changes, and network status changes.
This one-time route matches the stable-view/stable-manager model:
- view/model subscriptions live for the manager lifetime;
- each callback asks for the current active network;
- if no current network is active, it no-ops;
- inbound subscriptions remain per-network because each `Network` emits events independently.
This also makes N follow-up executions behave like a sequence of network replacements rather than a growing chain of listeners.
### Make ambient session end resumable
Permanent viewer teardown is split from ambient execution-ended handling. Normal shared-session viewers still keep today's behavior: ended banner, read-only input, finished viewer state, and no future attach path.
For shared ambient agent sessions, `SessionEnded` is treated as a resumable execution boundary. `ambient_session_ended` unregisters the live shared-session transport from `Manager`, clears the write-to-PTY sender, and clears `current_network` if it still points at the ended session. It does not call `TerminalView::on_session_share_ended`, does not set `SharedSessionStatus::FinishedViewer`, and does not cancel the ambient conversation, so a later `attach_followup_session` can reuse the same view/model.
### Add ambient follow-up attach event
The ambient view model emits `SessionReady { session_id }` for the initial session and `FollowupSessionReady { session_id }` for later fresh session IDs.
The subscription in `create_cloud_mode_view` dispatches initial sessions to `connect_to_session` and follow-up sessions to `attach_followup_session`.
The “continue” trigger and server API for creating the follow-up execution can land separately, but the session-sharing infra should expose a narrow API that only needs a new `SessionId`.
### Add append-aware scrollback loading
The viewer event loop accepts a `SharedSessionInitialLoadMode`:
- `ReplaceFromSessionScrollback` for initial joins;
- `AppendFollowupScrollback` for ambient follow-up joins.
For initial joins, preserve the current call to `load_shared_session_scrollback`.
For follow-up joins, `EventLoop::new` calls the model/blocklist append path:
`append_followup_shared_session_scrollback(scrollback, is_alt_screen_active)`.
That method:
- finish any previous active block that cannot continue receiving output from the old session;
- mark the model bootstrapped/view-pending for the new session;
- skip scrollback blocks already present in the current blocklist;
- append only new blocks from the follow-up session;
- preserve block ID to block index mappings without duplicates;
- restore the new session's active block as the live block for subsequent ordered terminal events;
- send the same wakeup/refresh signals the current initial load path sends.
The dedupe contract should be explicit. The preferred contract is that the follow-up VM preserves `SerializedBlock.id` for rehydrated prior blocks. Then client-side dedupe is deterministic: skip incoming scrollback blocks whose IDs are already present, and append from the first unknown block onward. If the follow-up VM cannot preserve block IDs, the handoff/session producer needs to provide continuation-only scrollback or a join-payload continuation marker. Without one of those contracts, the client cannot reliably distinguish “old output replayed in a new session” from “new output that happens to look identical.”
### Keep conversation identity stable
`SessionSourceType::AmbientAgent { task_id }` currently drives `AmbientAgentViewModel::enter_viewing_existing_session` and `ActiveAgentViewsModel::register_ambient_session` in `app/src/terminal/shared_session/viewer/terminal_manager.rs (624-666)`. Follow-up executions are guaranteed to keep the same run ID, so the ambient view should continue to use the existing task/run identity across session attachments.
The new session ID is only a new transport for the same ambient run, not a new active conversation. `ActiveAgentViewsModel` continues pointing the same terminal view at the same ambient task ID, and follow-up attach updates session transport state without changing the user-visible conversation identity.
## End-to-end flow
1. User starts an ambient cloud conversation.
2. `AmbientAgentViewModel` emits initial `SessionReady`.
3. `viewer::TerminalManager` attaches the first session with initial load mode.
4. The cloud VM finishes; the session-sharing server sends `SessionEnded`.
5. The viewer manager records an ambient execution-ended state and removes the active network without permanently poisoning the view.
6. User clicks “continue” and sends a follow-up prompt.
7. The follow-up orchestration creates a new VM/session and returns a new `SessionId`.
8. `AmbientAgentViewModel` emits a follow-up session-ready event.
9. The existing terminal manager creates a fresh `Network` for the new session ID.
10. The new event loop loads follow-up scrollback in append mode, skipping already-known blocks.
11. New ordered terminal events stream into the same blocklist and ambient view.
12. Steps 4-11 can repeat for N follow-up executions.
## Risks and mitigations
The highest-risk area is scrollback dedupe. Mitigate by making the rehydration contract explicit before implementation: either preserve block IDs, send continuation-only scrollback, or add a continuation marker. Do not rely on byte/string comparisons of terminal output.
Replacing networks without cleaning subscriptions can leak old networks and duplicate outbound messages. Mitigate by routing all outbound subscriptions through one stable current-network handle and testing repeated follow-ups.
Treating ambient `SessionEnded` as resumable may regress normal shared-session viewers if the paths are not cleanly separated. Mitigate with an explicit ambient-only branch keyed off `TerminalModel::is_shared_ambient_agent_session()` or the attach kind, plus regression coverage for non-ambient viewers.
Conversation identity should be stable because follow-up executions reuse the same run ID. The main risk is accidentally treating the new session ID as a new conversation identity; mitigate by keeping `ActiveAgentViewsModel` registration tied to the existing ambient task/run and treating session IDs as transport state only.
## Testing and validation
Targeted event-loop/blocklist coverage was added for:
- follow-up load skips scrollback blocks whose IDs already exist;
- follow-up load appends new blocks in order;
- duplicate block IDs do not corrupt `block_id_to_block_index`.
Additional follow-up work should add direct `viewer::TerminalManager` coverage for:
- initial deferred attach creates one active network;
- ambient `SessionEnded` transitions to resumable ended state;
- follow-up attach replaces the old network with a new session ID;
- repeated follow-up attach does not duplicate outbound sends;
- non-ambient `SessionEnded` still produces finished/read-only viewer behavior.
Additional integration coverage should exercise the ambient flow:
- start cloud mode and join an initial ambient session;
- simulate session ended;
- attach a second session ID into the same terminal view;
- assert old output remains and new output appears after it;
- repeat with a third session ID;
- assert the same ambient view remains visible and active throughout.
Manual validation should exercise a real cloud-mode conversation once the follow-up API exists: start a cloud agent, wait for the VM/session to end, click continue, confirm a new VM starts, and verify the same ambient view appends new output instead of opening a new transcript or replacing prior blocks.
## Parallelization
The work can split across three mostly independent tracks:
- viewer manager lifecycle: reusable resources, current-network replacement, ambient-ended state, and outbound subscription routing;
- blocklist/event-loop append mode: dedupe contract, append loader, and event-loop load mode tests;
- ambient follow-up trigger: server/API integration for “continue,” model event wiring, and stable conversation identity.
The final integration point is the ambient model handing a fresh follow-up `SessionId` to the viewer manager attach API.