70 lines
13 KiB
Markdown
70 lines
13 KiB
Markdown
# Cloud-to-cloud handoff PR 2 tech spec
|
||
## Problem statement
|
||
PR 2 should add the orchestration layer that turns an existing cloud agent run into a follow-up execution and hands the resulting fresh shared session to the hotswap path. This PR should remain mergeable while `HandoffCloudCloud` is disabled by default, and it should not add the user-visible tombstone Continue button or terminal-input submission route yet.
|
||
The intended boundary is model/API behavior: a future UI can call one ambient-model method with a follow-up prompt, the client submits `POST agent/runs/{runId}/followups`, polls the same run until a new joinable session appears, ignores the ended session, and emits `FollowupSessionReady` so the existing viewer manager attaches the new session in append mode.
|
||
## Current state
|
||
PR 1 added the disabled `HandoffCloudCloud` flag and encoded the Cargo feature dependency on `cloud_mode_setup_v2` in `app/Cargo.toml:924`. It also added `RunFollowupRequest`, `AIClient::submit_run_followup`, and `build_run_followup_url` in `app/src/server/server_api/ai.rs (211-216, 837-840, 1467-1471)`, with endpoint/serialization tests in `app/src/server/server_api/ai_test.rs (988-1000)`.
|
||
`AmbientAgentTask` now has `run_id()`, `conversation_id()`, and `active_run_execution()` accessors that project the current flattened response fields into a `RunExecution` view in `app/src/ai/ambient_agents/task.rs (247-279)`. `SessionJoinInfo::from_task` already consumes that projection in `app/src/ai/ambient_agents/spawn.rs (31-57)`.
|
||
Initial cloud startup is still a single combined helper: `spawn_task` creates a run, polls `get_ambient_agent_task`, emits state changes, and ends when the first session is joinable in `app/src/ai/ambient_agents/spawn.rs (85-176)`. There is no reusable “poll an existing run until a new execution session is ready” helper yet.
|
||
`AmbientAgentViewModel` still models startup as `Status::WaitingForSession { progress }` without distinguishing an initial run from a follow-up execution in `app/src/terminal/view/ambient_agent/model.rs (50-64)`. The existing `SessionStarted` handler infers follow-up readiness from being already in `AgentRunning`, which is too implicit for a model-driven follow-up flow in `app/src/terminal/view/ambient_agent/model.rs (627-638)`. The current `attach_followup_session` method simply emits `FollowupSessionReady` for a known session ID, which is useful test scaffolding but does not submit or poll a follow-up in `app/src/terminal/view/ambient_agent/model.rs:349`.
|
||
The hotswap receiver already exists. `create_cloud_mode_view` routes `SessionReady` to `connect_to_session` and `FollowupSessionReady` to `attach_followup_session` in `app/src/terminal/view/ambient_agent/mod.rs (69-82)`. The viewer manager’s follow-up attach path replaces the active network and joins with append-mode scrollback in `app/src/terminal/shared_session/viewer/terminal_manager.rs (338-384)`.
|
||
The UI has important side effects tied to initial dispatch. `DispatchedAgent` inserts the initial optimistic user query in `TerminalView::handle_ambient_agent_event` and drives the ambient entry-block insertion subscription in `app/src/terminal/view/ambient_agent/view_impl.rs (105-131, 445-481)`. PR 2 should avoid reusing that event for follow-ups, because doing so would blur initial-run and follow-up behavior before the UX PR.
|
||
## Goals
|
||
Add reusable follow-up orchestration that submits a prompt to an existing run and waits for a fresh active execution session.
|
||
Make the ambient view model explicitly track whether it is waiting for an initial session or a follow-up session.
|
||
Track the active or previous execution session ID so follow-up polling can ignore stale readiness from the ended session.
|
||
Emit the already-supported `FollowupSessionReady` event when the new session is ready, allowing the existing hotswap path to attach it.
|
||
Reuse existing Cloud Mode setup/loading/error state machinery for follow-up waiting and failures, but without adding a visible Continue entrypoint.
|
||
Keep the implementation behind `FeatureFlag::HandoffCloudCloud` and preserve behavior with the flag off.
|
||
## Non-goals
|
||
No tombstone Continue button, action, or copy changes.
|
||
No terminal input routing changes for submitting follow-up prompts.
|
||
No embedded follow-up prompt editor in the tombstone.
|
||
No product decision on tombstone stacking or update-in-place behavior.
|
||
No first-class server execution-array parsing unless the public API response shape already exposes it in this branch.
|
||
No rollout enablement for `HandoffCloudCloud`.
|
||
## Proposed changes
|
||
### Reusable run polling and follow-up helper
|
||
Refactor `spawn_task` in `app/src/ai/ambient_agents/spawn.rs` so run creation and run readiness monitoring are separate. Keep the public `spawn_task(request, ai_client, timeout)` behavior the same by having it call a new internal polling helper after `spawn_agent` succeeds.
|
||
Add a helper such as `poll_run_until_joinable_session(run_id, ai_client, previous_session_id, timeout)` that repeatedly calls `get_ambient_agent_task(&run_id)`, emits `StateChanged` when state changes, and returns `SessionStarted` only when the task is `InProgress` and `SessionJoinInfo::from_task` contains a parseable `session_id` that differs from `previous_session_id` when one was provided.
|
||
Add a follow-up stream/helper such as `submit_run_followup(prompt, run_id, previous_session_id, ai_client, timeout)`. It should call `AIClient::submit_run_followup(run_id, RunFollowupRequest { message: prompt })` first, then call the polling helper. API failure before acceptance should yield an error without polling. Polling errors should surface through the same error path as initial spawn.
|
||
For initial spawn, preserve the existing tolerance for a session link without a parsed session ID if any caller still needs that metadata. For follow-up readiness, require a parsed session ID because the hotswap API needs a `SessionId`.
|
||
Terminal states before a fresh session is found should not leave the follow-up wait indefinitely. Failure-like states should emit the state change and then surface the task status message as an error; successful terminal completion without a new session should complete with a clear “no follow-up session became available” error.
|
||
### Explicit ambient model startup kind
|
||
Add a small enum such as `SessionStartupKind { InitialRun, Followup }` and change `Status::WaitingForSession` to carry `{ progress, kind }`. Existing accessors like `agent_progress()` and `is_waiting_for_session()` should remain behavior-preserving.
|
||
Add fields to `AmbientAgentViewModel` for follow-up bookkeeping: the active execution `SessionId`, the last ended execution `SessionId` if available, and the currently submitted follow-up prompt. The prompt field is for PR 3’s optimistic rendering; PR 2 should store it but not insert a visible follow-up query block.
|
||
Update initial spawn to set `WaitingForSession { kind: InitialRun }`. Update `AmbientAgentEvent::SessionStarted` handling to emit `SessionReady` for `InitialRun` and `FollowupSessionReady` for `Followup`, rather than relying on whether the current status happens to be `AgentRunning`.
|
||
Add `AmbientAgentViewModel::submit_cloud_followup(prompt, ctx)`. It should require `FeatureFlag::HandoffCloudCloud`, require an existing `task_id`/run ID, capture the previous active or ended session ID, set `WaitingForSession { kind: Followup }`, start the progress timer, store the pending prompt, emit a distinct follow-up dispatch event, and spawn the follow-up helper stream.
|
||
On follow-up success, stop the timer, set `status` to `AgentRunning`, update the active execution session ID, clear the pending prompt, and emit `FollowupSessionReady { session_id }`. On failure, reuse the existing failure/auth/quota/capacity mapping logic as much as possible so follow-up setup errors render through the same state as initial setup errors.
|
||
### Execution-ended bookkeeping without visible UI
|
||
Extend the ambient session-ended path only enough for bookkeeping. `viewer::TerminalManager::ambient_session_ended` currently leaves the pane resumable and clears the active network in `app/src/terminal/shared_session/viewer/terminal_manager.rs (1490-1515)`. In PR 2 it can notify the ambient view model of the ended session ID behind `HandoffCloudCloud`, so the model records `last_ended_execution_session_id` and can reject duplicate readiness from that session.
|
||
This notification should not call `TerminalView::on_session_share_ended`, should not insert a tombstone, should not set `SharedSessionStatus::FinishedViewer`, and should not cancel the local conversation. Those UI and lifecycle decisions remain PR 3 scope.
|
||
### Event and view integration
|
||
Add a new model event such as `FollowupDispatched` instead of reusing `DispatchedAgent`. `create_cloud_mode_view` only needs an exhaustive-match update for the new event because `FollowupSessionReady` is already wired to `attach_followup_session`.
|
||
Update `TerminalView::handle_ambient_agent_event` to handle `FollowupDispatched` by notifying/re-rendering progress UI and marking the active ambient conversation as `ConversationStatus::InProgress` if one exists. It should not insert `CloudModeInitialUserQuery`, should not insert a second `AmbientAgentEntryBlock`, and should not auto-open new UI beyond the existing setup/progress rendering.
|
||
The existing loading screen in `app/src/terminal/view/ambient_agent/view_impl.rs (529-571)` can continue to derive messages from `AgentProgress` for PR 2. If copy changes are desired for follow-ups, keep them minimal and keyed off `SessionStartupKind`, but deferring user-facing copy to PR 3 is acceptable.
|
||
## Testing strategy
|
||
Add stream-level tests in `app/src/ai/ambient_agents/spawn_tests.rs` covering follow-up submission and polling. The important cases are: the helper calls `submit_run_followup` before polling; it ignores the previous session ID returned by the server; it emits `SessionStarted` for a different new session ID; it propagates API errors before polling; it surfaces terminal failure before readiness.
|
||
Preserve existing `spawn_task` tests so initial spawn behavior remains unchanged after the refactor.
|
||
Add model-level tests if there is a lightweight existing harness for `AmbientAgentViewModel`; otherwise keep model changes small and validate via stream tests plus targeted compile checks. Model assertions should cover `submit_cloud_followup` preconditions, `WaitingForSession { kind: Followup }`, and `FollowupSessionReady` emission on a fresh session.
|
||
Run targeted validation after implementation: `cargo nextest run -p warp ai::ambient_agents::spawn::tests server::server_api::ai::tests::build_run_followup_url_routes_to_run_followups server::server_api::ai::tests::serialize_run_followup_request` and `cargo check -p warp --features handoff_cloud_cloud`. If model or terminal-view tests are added, include their module filters. Do not use `cargo fmt --all` or file-specific `cargo fmt`; use the repo’s standard formatting command only when preparing a PR update.
|
||
## Rollout and compatibility
|
||
With `HandoffCloudCloud` off, no production UI should call the new follow-up method and existing initial Cloud Mode startup should behave as it does today.
|
||
With the flag on, PR 2 only exposes an internal/model-level follow-up path. The absence of a visible entrypoint makes this safe to merge before product UX lands, while unit tests can still exercise the orchestration path.
|
||
The runtime code may assume `CloudModeSetupV2` when `HandoffCloudCloud` is enabled because the Cargo feature dependency was added in PR 1.
|
||
## Risks and mitigations
|
||
The server may briefly return the ended execution’s session fields after accepting a follow-up. Mitigate by passing the previous session ID into the polling helper and requiring a different parsed session ID before emitting readiness.
|
||
Reusing `DispatchedAgent` for follow-ups would insert initial-run UI artifacts again. Mitigate with a distinct follow-up event and explicit startup kind.
|
||
Refactoring `spawn_task` could regress initial Cloud Mode startup. Mitigate by preserving the public stream contract and keeping existing spawn tests green.
|
||
A follow-up may be accepted but fail before any session becomes joinable. Mitigate by reusing the existing failed/auth/quota/capacity UI states and leaving future UI free to retry from the tombstone in PR 3.
|
||
Model bookkeeping could drift if session-ended notifications are missed. Mitigate by also falling back to the last active execution session ID when submitting a follow-up.
|
||
## Parallelization
|
||
This PR is small enough to implement sequentially, but two independent tracks could run in parallel if needed. One track can refactor and test `spawn.rs` follow-up polling with mocked `AIClient`; the other can wire `AmbientAgentViewModel` state/events and terminal-manager bookkeeping. They converge at `submit_cloud_followup` consuming the follow-up helper and emitting `FollowupSessionReady`.
|
||
## Definition of done
|
||
`spawn_task` still behaves the same for initial runs after extracting reusable polling.
|
||
A follow-up helper submits a prompt, polls the stable run, ignores stale session IDs, and returns a fresh joinable session.
|
||
`AmbientAgentViewModel::submit_cloud_followup` exists behind `HandoffCloudCloud` and drives `WaitingForSession { kind: Followup }` through success and error states.
|
||
`FollowupSessionReady` is emitted for fresh sessions and continues to attach through the existing hotswap path.
|
||
No tombstone Continue UI or terminal-input follow-up route is added in this PR.
|
||
Targeted tests and `cargo check -p warp --features handoff_cloud_cloud` pass.
|