Files
galaxy/specs/ambient-agent-rtc-refresh-throttle/TECH.md
T

5.0 KiB

Ambient agent RTC refresh throttling — Tech Spec

Context

Ambient agent task updates arrive through the Warp Drive RTC subscription. WarpDriveUpdate::AmbientTaskUpdated is converted into ObjectUpdateMessage::AmbientTaskUpdated in app/src/workspaces/gql_convert.rs:1124, then forwarded through Listener::get_warp_drive_updates in app/src/server/cloud_objects/listener.rs:351. UpdateManager::received_message_from_server gates the event on FeatureFlag::AmbientAgentsRTC in app/src/server/cloud_objects/update_manager.rs:1278 and emits UpdateManagerEvent::AmbientTaskUpdated { timestamp } from handle_ambient_task_changed in app/src/server/cloud_objects/update_manager.rs:1119. AgentConversationsModel::new subscribes to UpdateManager when AmbientAgentsRTC is enabled in app/src/ai/agent_conversations_model.rs:568. Today AgentConversationsModel::handle_update_manager_event immediately calls fetch_tasks_updated_after for every task update event in app/src/ai/agent_conversations_model.rs:641. That method calls AIClient::list_ambient_agent_tasks with TaskListFilter { updated_after: Some(timestamp - 1 second) }, which maps to GET /api/v1/agent/runs in app/src/server/server_api/ai.rs:1726. This creates request volume proportional to team-wide ambient task update volume. For users in large Warp teams, many unrelated cloud agent task updates can trigger many list-run requests from every client. The non-RTC periodic polling path in AgentConversationsModel::poll_for_tasks uses a 30 second interval and is disabled while AmbientAgentsRTC is enabled in should_be_polling at app/src/ai/agent_conversations_model.rs:893. The orchestration viewer has a 5 second polling cadence in app/src/terminal/shared_session/viewer/orchestration_viewer_model.rs, but it is not an event-stream throttle and should not be reused directly.

Proposed changes

Add an RTC-only leading/trailing throttle inside AgentConversationsModel. Keep Listener and UpdateManager as generic event plumbing; the request-volume policy belongs at the single consumer that owns list_ambient_agent_tasks. Add a module-local constant:

  • RTC_TASK_REFRESH_THROTTLE: Duration = Duration::from_secs(5) Add an internal throttle state to AgentConversationsModel:
  • Idle when no throttle window is active.
  • CoolingDown { pending_timestamp, timer_abort_handle } after a refresh has fired and the 5 second cooldown timer is active. When handle_update_manager_event receives UpdateManagerEvent::AmbientTaskUpdated:
  • If state is Idle, call fetch_tasks_updated_after(timestamp, ctx) immediately and start a 5 second cooldown timer.
  • If state is CoolingDown, merge the event into pending_timestamp without issuing a request. When the cooldown timer expires:
  • If pending_timestamp is present, call fetch_tasks_updated_after(pending_timestamp, ctx) and start a new cooldown timer.
  • If there is no pending timestamp, return to Idle. The pending timestamp must be the earliest timestamp observed during the cooldown window. The server filter is updated_after, so latest-timestamp coalescing could skip runs updated earlier in the window after the leading request began. Earliest-timestamp coalescing may over-fetch a small window, but it preserves correctness and still caps request volume. Leave fetch_tasks_updated_after as the raw fetch method so initial sync, manual filter fetches, and future direct callers do not inherit RTC throttling accidentally. Keep the existing 1 second subtraction buffer because the server filter is strict and server/client clocks can differ. On reset, abort any active RTC throttle timer and clear pending state to avoid a trailing fetch after logout. Keep the existing spawn_with_retry_on_error behavior for each logical fetch; this change caps logical fetch starts, and retry policy can be revisited separately if backend attempt volume remains too high.

Testing and validation

Add focused unit coverage for the throttle's timestamp coalescing helper:

  • no pending timestamp records the first timestamp;
  • later timestamps do not replace the earliest pending timestamp;
  • earlier timestamps replace the pending timestamp. If a future patch adds an injectable AIClient seam or fake clock for AgentConversationsModel, add model-level tests for:
  • a burst of RTC events causes one immediate fetch and one trailing fetch;
  • the trailing fetch uses the earliest pending timestamp;
  • reset cancels any pending trailing refresh;
  • RTC disabled keeps the existing periodic polling behavior unchanged. Run targeted validation:
  • cargo nextest run -E 'test(rtc_task_refresh)'
  • cargo check -p warp Before opening or updating a PR, run the repository-required format and clippy checks. Do not use cargo fmt --all or file-specific cargo fmt.

Parallelization

Do not split implementation across sub-agents. The change is small and tightly coupled: the spec, throttle state, event handling, reset cleanup, and tests all live around AgentConversationsModel. Parallel edits would touch the same files and add merge overhead without meaningful wall-clock savings.