13 KiB
Cloud agent tombstone and followup input behavior — Tech Spec
Product spec: specs/APP-4483/PRODUCT.md
Context
PRODUCT.md defines behavior invariants B1–B6. This technical spec implements those invariants when FeatureFlag::HandoffCloudCloud is enabled, while preserving existing behavior when that flag is disabled.
The current UI decision points are split across three places:
app/src/terminal/view/shared_session/view_impl.rs:729computesviewed_ambient_task_id_owned_by_current_userand uses task creator ownership to decide whetheron_session_share_endedinserts a tombstone.app/src/terminal/view/shared_session/view_impl.rs:779enables the followup input only when the current user owns the ambient task.app/src/terminal/view/shared_session/view_impl.rs:811repeats the same ownership model forhandle_non_running_ambient_agent_task. There is a second delayed path for already-loaded ambient tasks:app/src/terminal/view.rs:7123checks whether a non-running shared ambient task should get a tombstone.app/src/terminal/view.rs:7149again usesowned_ambient_agent_task_idplusHandoffCloudCloudto decide whether to show the input instead. The tombstone currently makes an independent CTA decision:app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:207creates aContinuecloud button whenever a task id exists andHandoffCloudCloudis enabled.app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:221creates aContinue locallybutton whenever a conversation id exists.app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:473hides local continuation for known non-Oz harnesses, but treats unknown harness metadata as local-continuable. The data needed for the new product model is already present:ServerAIConversationMetadatacontainsharnessandpermissionsinapp/src/ai/agent/conversation.rs:3849.AIAgentHarnessdistinguishes Oz, Claude Code, Gemini, Codex, and Unknown inapp/src/ai/agent/conversation.rs:3821.BlocklistAIHistoryModel::get_server_conversation_metadataalready looks up loaded conversation metadata with a fallback to cached conversation metadata inapp/src/ai/blocklist/history_model.rs:1986.AgentConversationsModel::fetch_ambient_agent_tasks_and_cloud_convo_metadatafetches ambient tasks and cloud conversation metadata together, including additional metadata for task conversation IDs missing from the first metadata response, inapp/src/ai/agent_conversations_model.rs:675.AmbientAgentTaskexposesconversation_id, active execution state, and whether cloud followup submission is allowed inapp/src/ai/ambient_agents/task.rs:317.SharingAccessLevelis ordered as View < Edit < Full incrates/warp_server_client/src/drive/sharing.rs:8. There is an object-access precedent, but it is tied to loaded Warp Drive objects:CloudViewModel::access_leveldefaults missing objects to view access inapp/src/cloud_object/model/view.rs:173.CloudViewModel::object_access_levelgrants full access for personal/team-space objects, applies link and guest ACLs for shared-space objects, and upgrades creator access to edit inapp/src/cloud_object/model/view.rs:181. The APP-4483 implementation should reuse the same permission semantics where possible, but must operate directly onServerAIConversationMetadata.permissionsbecause AI conversation metadata is not aCloudObject.
Proposed changes
1. Add a single continuation UI state resolver
Add a small helper module under app/src/terminal/view/shared_session/, for example cloud_conversation_continuation.rs, and use it from all tombstone/followup decision paths.
Suggested core types:
ContinuationHarness:Oz,ThirdParty, orUnknown.ConversationAccess:Edit,ViewOnly, orUnknown.TombstoneCta:ContinueLocally,ContinueInCloud { task_id }, or none.CloudConversationContinuationUiState, containing:- whether the ended-state tombstone should be present;
- whether the inline cloud followup input should be enabled;
- the task id to use for cloud followup input submission, if any;
- the tombstone CTA to render, if any.
The resolver should accept the terminal view id, optional ambient task id, whether a live shared session is still active, and
AppContext. It should derive:
- active execution from the ambient task when present;
- harness from
ServerAIConversationMetadata.harness, not from tombstone display metadata; - edit access from
ServerAIConversationMetadata.permissions; - unknown state when task or conversation metadata is unavailable. Mapping:
- B1: Oz + edit + no active execution returns no tombstone and followup input enabled for that task.
- B2: Oz + view-only + no active execution returns tombstone with
ContinueLocally. - B3: third-party + edit + no active execution returns tombstone with
ContinueInCloud. - B4: third-party + view-only + no active execution returns tombstone with no CTA.
- B5: any harness/access + active execution or live shared session returns no ended-state tombstone and no followup input transition.
- B6: unknown harness or unknown access + no active execution returns tombstone with no CTA.
When
FeatureFlag::HandoffCloudCloudis disabled, keep the existing pre-APP-4483 behavior path: ended ambient sessions may show the tombstone, but the permission-aware cloud followup state should not be used.
2. Resolve conversation metadata by server token
Add a BlocklistAIHistoryModel helper that returns ServerAIConversationMetadata by ServerConversationToken, for example:
- check
server_token_to_conversation_id; - if found, reuse
get_server_conversation_metadata; - otherwise scan
all_conversations_metadatafor matchingserver_conversation_token; - return
Noneif metadata is not loaded. This avoids forcing callers to manufacture or resolve anAIConversationIdbefore they can inspect task-linked conversation permissions.AmbientAgentTask::conversation_idreturns the token string atapp/src/ai/ambient_agents/task.rs:317, so the resolver can construct aServerConversationTokenfrom that value and ask the history model for metadata. If metadata is missing, do not fetch synchronously from the UI resolver. Treat the state as B6 and let the existingAgentConversationsModelfetch path populate metadata asynchronously.
3. Compute edit access from ServerPermissions
Add a pure helper near the resolver or in a small permission utility that computes the current user's effective SharingAccessLevel from ServerAIConversationMetadata.
Rules:
- If the current user is missing or logged out, start at
SharingAccessLevel::View. - If
permissions.spaceisOwner::Userfor the current user, return at leastFull. - If
permissions.spaceisOwner::Teamand the team appears inUserWorkspaces::team_from_uid_across_all_workspaces, return at leastFull. - Apply
permissions.anyone_link_sharingas a baseline when present. - Apply user guest ACLs when
ServerGuestSubject::User { firebase_uid }matches the current user. - Apply team guest ACLs when the current user belongs to the guest team according to
UserWorkspaces. - Ignore pending-user ACLs for this UI decision.
- If
metadata.creator_uidmatches the current user, upgrade to at leastEdit, matching the creator fallback inCloudViewModel::object_access_level. - Return
ConversationAccess::EditforEditorFull; returnViewOnlyforView. If team membership data is not loaded and access is only knowable through a team owner/guest ACL, do not assume edit access. The safe UI state remains view-only/unknown until workspace metadata is available.
4. Replace ownership-based UI branching
Replace the creator-owned task gate in these call sites with the resolver:
on_session_share_endedinapp/src/terminal/view/shared_session/view_impl.rs:727.handle_non_running_ambient_agent_taskinapp/src/terminal/view/shared_session/view_impl.rs:811.maybe_insert_tombstone_for_non_running_shared_ambient_taskinapp/src/terminal/view.rs:7123. Add a shared method onTerminalView, for examplerefresh_non_running_cloud_agent_continuation_ui, that:- exits early for disabled
CloudModeSetupV2, active replay, existing pending cloud followup submission, or disabledHandoffCloudCloud; - asks the resolver for the current state;
- removes an existing tombstone when the new state is B1 and enables the cloud followup input;
- inserts or updates the tombstone when the new state is B2, B3, B4, or B6;
- keeps input selectable/read-only for viewers when no continuation input should be shown.
enable_owned_cloud_followup_inputshould be renamed or wrapped with a permission-neutral name, such asenable_cloud_followup_input, because it will now be used for users with edit access who may not be the task creator. Updatetry_submit_pending_cloud_followupinapp/src/terminal/view.rs:20026so it does not fall back toowned_ambient_agent_task_id. Submission should use an explicitpending_cloud_followup_task_idor a task id that the resolver stored when it enabled the inline input. This prevents creator ownership from remaining a hidden permission bypass.
5. Make tombstone CTAs data-driven
Change ConversationEndedTombstoneView::new to accept a CTA decision from the resolver rather than constructing both buttons from task_id and conversation_id.
The tombstone should render:
Continue locallyonly forTombstoneCta::ContinueLocally.Continueonly forTombstoneCta::ContinueInCloud { task_id }.- no button when the CTA is absent.
Keep
ConversationEndedTombstoneEvent::ContinueInCloudandstart_cloud_followup_from_tombstonefor B3. KeepContinueLocallybehavior for B2. KeepTombstoneDisplayData::enrich_from_taskfor display metadata only; it should no longer decide CTA visibility after an async task fetch. This removes the current mismatch whereContinueis shown for any task id andContinue locallyis shown for unknown harness metadata.
6. Recompute when metadata changes
Permissions and metadata can arrive after the tombstone is first inserted. Recompute the continuation UI state when:
AgentConversationsModelEvent::TasksUpdatedupdates task active-execution state or task conversation id;AgentConversationsModelEvent::ConversationsLoadedmerges cloud conversation metadata;BlocklistAIHistoryEvent::UpdatedConversationMetadataupdates server metadata for a live conversation. If recomputation transitions:- from B6 to B1, remove the tombstone and enable the input;
- from B6 to B2/B3/B4, update or reinsert the tombstone with the correct CTA;
- from B1 to a non-edit state, clear/disable the input and show the tombstone state for the latest access. Use the resolver as the only source of truth for these transitions.
Testing and validation
Add focused unit tests for the resolver and update existing shared-session tests so validation maps directly to PRODUCT.md B1–B6:
- B1: Oz metadata + edit access + ended execution produces no tombstone, editable followup input, and cloud submission uses the same task id even when task creator is someone else.
- B2: Oz metadata + view-only access produces a tombstone with
Continue locally, no inline followup input, and no cloud CTA. - B3: Claude Code/Codex/Gemini metadata + edit access produces a tombstone with
Continue, and clicking it enters the existing cloud followup flow. - B4: third-party metadata + view-only access produces a tombstone with no continue CTA.
- B5: active execution or live shared session does not insert an ended-state tombstone regardless of harness/access.
- B6: missing metadata, unknown harness, or unknown access produces a tombstone with no mutation CTA. Permission helper tests should cover:
- user owner;
- team owner with current-user team membership;
- user guest view vs edit;
- team guest view vs edit;
- link sharing view only;
- creator fallback to edit;
- missing current user defaults to non-edit.
Update or replace creator-based assertions in
app/src/terminal/view/shared_session/view_impl_tests.rs, especially the tests currently named around “owned” ambient sessions. Add tombstone CTA tests inapp/src/terminal/view/shared_session/conversation_ended_tombstone_view_tests.rsonce CTA state is data-driven. Suggested targeted commands: cargo test -p warp --lib terminal::view::shared_session::view_impl_testscargo test -p warp --lib terminal::view::shared_session::conversation_ended_tombstone_view_testscargo test -p warp --lib terminal::view::shared_session::cloud_conversation_continuationBefore PR/update, run the repository-required format and clippy checks from the PR workflow. Do not usecargo fmt --allor file-specificcargo fmt.
Parallelization
Do not split this implementation across sub-agents. The changes are tightly coupled across one UI state resolver, terminal view lifecycle transitions, tombstone CTA rendering, and existing shared-session tests. Parallel edits would likely touch the same files and increase merge overhead more than they reduce wall-clock time.