Skip to content

QUALITY-780: client core — WaitingForEvents status variant + predicates + persistence (Wave 1)#11998

Draft
cephalonaut wants to merge 13 commits into
masterfrom
matthew/QUALITY-780-client-core
Draft

QUALITY-780: client core — WaitingForEvents status variant + predicates + persistence (Wave 1)#11998
cephalonaut wants to merge 13 commits into
masterfrom
matthew/QUALITY-780-client-core

Conversation

@cephalonaut
Copy link
Copy Markdown
Contributor

@cephalonaut cephalonaut commented Jun 1, 2026

Description

Wave 1 of QUALITY-780: adds the first-class WaitingForEvents conversation status, the is_terminal / is_quiescent predicate split (removing is_done), and the persistence round-trip for the yielded state. Bumps warp_multi_agent_api to the Wave 0 proto SHA (792d5878).

Per the client TECH spec sections §1–§3:

  • §1 ConversationStatus::WaitingForEvents variant with Display label "Waiting for events", Icon::ClockSnooze, and ansi_fg_blue() / ansi_bg_blue() colors. Adds ansi_bg_blue() to WarpTheme and blue_listening_icon() to icons.rs. TODO(design) markers flag the final visual.
  • §2 is_terminal() (Success | Error | Cancelled) and is_quiescent() (!InProgress) replace the legacy is_done() predicate. Migrates all five §2.1 callers to is_terminal(): conversation-list view, command palette fork item + data_source, /cost slash command, and the orchestration pill bar overflow menu (delete-vs-kill gating). Adds an is_waiting_for_events() probe. Drops is_done() entirely with a comment explaining the split.
  • §3 AgentConversationData.waiting_for_events bool (#[serde(default, skip_serializing_if = "is_false")]) round-trips through write_updated_conversation_state and new_restored_synthesizing_on_empty. The restore-site override applies to both the empty-tasks and populated-tasks branches so a child conversation persisted while yielded re-enters WaitingForEvents on next launch instead of being silently reclassified as Success by derive_status_from_root_task.

Wave 2 placeholder arms with TODO(client-{driver,sync-notif,pill-bar,detection}) comments are added wherever the compiler surfaced a non-exhaustive match against ConversationStatus or the new ToolType::WaitForEvents / ToolCallResultType::WaitForEvents proto variants. local_agent_task_sync_model::map_conversation_status uses the final §5 shape (AgentTaskState::InProgress, None) per the prompt. orchestration_topology::aggregated_orchestrator_status and the pill bar sort key co-bucket WaitingForEvents with InProgress for now; the final §7 precedence ordering is owned by client-pill-bar.

This branch is the merge base for Wave 2 PRs (client-driver, client-sync-notif, client-pill-bar, client-detection); those PRs will target this branch rather than master.

Proto dependency: warpdotdev/warp-proto-apis#315

Linked Issue

QUALITY-780

  • The linked issue is labeled ready-to-spec or ready-to-implement.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).

Testing

New tests in app/src/ai/agent/conversation_tests.rs:

  • Display label asserts the placeholder text.
  • is_terminal / is_quiescent / is_waiting_for_events / is_in_progress matrix across all six variants.
  • Restore-site override fires in both populated- and empty-task paths.
  • Persistence JSON round-trip (true and false), default skip behavior, and legacy-row deserialization.

Validation run:

  • cargo fmt --check

  • cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warnings

  • cargo clippy -p warp_completer --all-targets --tests -- -D warnings

  • cargo nextest run -p warp -p persistence (4801 tests pass)

  • I have manually tested my changes locally with ./script/run

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Conversation: https://staging.warp.dev/conversation/bc28de12-e272-4eff-a89c-416e32888bd9
Run: https://oz.staging.warp.dev/runs/019e837c-4230-72a4-82ea-73e4abf89e77

This PR was generated with Oz.

…es + persistence (Wave 1)

Adds first-class WaitingForEvents conversation status, the is_terminal /
is_quiescent predicate split (removing is_done), and the persistence
round-trip for the yielded state. Bumps warp_multi_agent_api to the
Wave 0 proto SHA (792d5878).

Per the QUALITY-780 client TECH spec sections §1–§3:

- §1: ConversationStatus::WaitingForEvents variant with Display label
  "Waiting for events", Icon::ClockSnooze, and ansi_fg_blue() /
  ansi_bg_blue() colors. Adds ansi_bg_blue() to WarpTheme and
  blue_listening_icon() to icons.rs. TODO(design) markers flag final
  visual.

- §2: is_terminal() (Success | Error | Cancelled) and is_quiescent()
  (!InProgress) replace the legacy is_done() predicate. Migrates all
  five §2.1 callers to is_terminal(): conversation-list view, command
  palette fork item + data_source, /cost slash command, and the
  orchestration pill bar overflow menu (delete-vs-kill gating). Adds a
  is_waiting_for_events() probe. Drops is_done() entirely with a
  comment explaining the split.

- §3: AgentConversationData.waiting_for_events bool
  (#[serde(default, skip_serializing_if = is_false)]) round-trips
  through write_updated_conversation_state and
  new_restored_synthesizing_on_empty. The restore-site override applies
  to both the empty-tasks and populated-tasks branches so a child
  conversation persisted while yielded re-enters WaitingForEvents on
  next launch instead of being silently reclassified as Success by
  derive_status_from_root_task.

Wave 2 placeholder arms with TODO(client-{driver,sync-notif,pill-bar,
detection}) comments wherever the compiler surfaced a non-exhaustive
match against ConversationStatus or the new
ToolType::WaitForEvents / ToolCallResultType::WaitForEvents proto
variants. local_agent_task_sync_model::map_conversation_status uses the
final §5 shape (AgentTaskState::InProgress, None) per the prompt.
orchestration_topology::aggregated_orchestrator_status and the pill bar
sort key co-bucket WaitingForEvents with InProgress for now; the final
§7 precedence ordering is owned by client-pill-bar.

Test coverage in conversation_tests.rs:
- Display label asserts the placeholder text.
- is_terminal / is_quiescent / is_waiting_for_events / is_in_progress
  matrix across all six variants.
- Restore-site override fires in both populated- and empty-task paths.
- Persistence JSON round-trip (true and false), default skip behavior,
  and legacy-row deserialization.

Validation:
- cargo fmt --check
- cargo clippy --workspace --exclude warp_completer --all-targets
  --tests -- -D warnings
- cargo clippy -p warp_completer --all-targets --tests -- -D warnings
- cargo nextest run -p warp -p persistence (4801 tests pass)

This branch is the merge base for Wave 2 PRs (client-driver,
client-sync-notif, client-pill-bar, client-detection); Wave 2 PRs will
target this branch rather than master.

Proto dependency: warpdotdev/warp-proto-apis#315

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Jun 1, 2026
Implement client TECH §7 in the orchestration pill bar:

* aggregated_orchestrator_status now uses the full precedence
  `InProgress > Blocked > WaitingForEvents > Error > Cancelled > Success`.
  Adds a dedicated `any_waiting` accumulator instead of co-bucketing
  `WaitingForEvents` with `InProgress`, and updates the doc-comment
  precedence list.
* pill_status_sort_key gives `WaitingForEvents` its own explicit slot
  with the same sort key as `InProgress`, keeping yielded pills in the
  active half of the bar and strictly left of the done bucket.
* New precedence tests pin each WaitingForEvents boundary (covers
  PRODUCT.md (22)) and a sort-key test confirms WaitingForEvents shares
  the InProgress bucket and is < DONE_STATUS_KEY (covers (24)).

Co-Authored-By: Oz <oz-agent@warp.dev>
Implements client TECH §4: when the conversation transitions into
`ConversationStatus::WaitingForEvents`, the Oz CLI driver's
`UpdatedConversationStatus` handler now schedules a local watchdog timer
instead of resolving `run_exit` (so the worker stays alive across the
yield) and the watchdog uses the server-supplied `idle_timeout_seconds`
from the unresolved `wait_for_events` tool call, falling back to a new
`DEFAULT_ORCHESTRATED_IDLE_TIMEOUT_SECONDS` (30 minutes) when unset.

The watchdog is decoupled from `run_exit`: on fire it does NOT cancel
the run; instead it (will) emit a `WaitForEventsResult` tool-call result
that the agent's next turn processes. A `TODO(integration, QUALITY-780)`
marker at `emit_wait_for_events_timeout_result` documents the exact
helper signature client-detection's Wave 2 work will expose, so the
integrator can replace the stub with a single line.

A generation counter (mirroring the existing `IdleTimeoutSender`
pattern, but separate so the watchdog and the `idle_on_complete` timer
don't share state) supersedes the watchdog when the conversation
returns to `InProgress` via the normal resume path (per client TECH
§8.3).

`find_unresolved_wait_for_events_call` scans the conversation's
linearized messages for the most recent unresolved `WaitForEvents` tool
call, intentionally avoiding coupling to client-detection's internal
`mark_/clear_conversation_waiting_for_events` storage.

Tests cover:
- Default-timeout constant (30 minutes).
- `watchdog_timeout_for_call` honors positive server-supplied values,
  falls back to default for `0` (prost flattened-scalar unset), and
  clamps negative values to the default.
- `find_unresolved_wait_for_events_call` returns `None` when there's no
  `WaitForEvents` call, returns the call when present and unresolved,
  returns `None` when a later `ToolCallResult` matches the
  `tool_call_id`, and returns the most-recent unresolved call when
  multiple yields appear in the same conversation.
- Roundtrip: `idle_timeout_seconds=0` is preserved through the detection
  helper and resolved to the default at the timeout-helper boundary.

PRODUCT.md invariants (11), (12), (13) covered at the unit level. The
full subscription-driven scheduling/cancellation and end-to-end
fire-then-resume flows are intentionally deferred to the Wave 3
integration tests against the fake server (per client TECH
§"Testing and validation").

Co-Authored-By: Oz <oz-agent@warp.dev>
Implements client TECH §5 and §6 for QUALITY-780 by replacing the
WaitingForEvents placeholders left by client-core.

§5 — `local_agent_task_sync_model::map_conversation_status`: keep the
final shape `WaitingForEvents => (AgentTaskState::InProgress, None)`
and remove the client-sync-notif TODO marker on the comment now that
this is the canonical mapping.

§6 — `agent_management_model`:
- `handle_history_event_for_mailbox`'s `WaitingForEvents` arm now
  mirrors `InProgress` and clears any stale notification for the
  origin via `remove_notification_by_source`. No new toast fires for
  yielded conversations.
- `should_trigger_notification` is rewritten as an exhaustive match
  so a future `ConversationStatus` variant forces a deliberate
  decision instead of silently returning `false`. Behavior preserved:
  Success/Blocked/Error → true; InProgress/WaitingForEvents/Cancelled
  → false.

Also finalizes `AgentRunDisplayStatus::from_conversation_status` —
the conversation-list display keeps yielded runs in the
`ConversationInProgress` bucket, matching the task-sync mapping.
Visual differentiation of waiting vs streaming is owned by the
orchestration pill bar (client TECH §7), not this status enum.

Tests:
- `local_agent_task_sync_model_tests`: a new unit test routes a
  conversation through the history model to flip its status to
  `WaitingForEvents` and asserts `map_conversation_status` returns
  `(AgentTaskState::InProgress, None)`. A second test covers the
  `InProgress` mapping for completeness.
- `agent_management_model_tests`: six new pure-function tests cover
  every `ConversationStatus` variant for `should_trigger_notification`.
  Two end-to-end tests drive `UpdatedConversationStatus::Changed`
  through `update_status` and assert that a stale notification is
  cleared and no new toast is queued for `WaitingForEvents` and for
  the `WaitingForEvents → InProgress` resume.

Co-Authored-By: Oz <oz-agent@warp.dev>
cephalonaut and others added 9 commits June 1, 2026 14:24
Refreshes the warp_multi_agent_api dependency to pick up the Wave 0
proto follow-up that:

- Adds `WaitForEventsResult` as a top-level message (previously nested
  inside `Message::ToolCallResult`).
- Adds `WaitForEventsResult` as a variant in
  `Request::Input::ToolCallResult.result` oneof at field 35, so the
  client watchdog timeout result can round-trip across the wire.

No code changes needed in this Wave 1 branch: all Wave 1 references to
the new variants use variant-name patterns (`Tool::WaitForEvents(_)`,
`ToolCallResultType::WaitForEvents(_)`), not the inner message type, so
the `message::tool_call_result::WaitForEventsResult` ->
`WaitForEventsResult` rename is a no-op here.

Validation:
- cargo fmt --check
- cargo clippy --workspace --exclude warp_completer --all-targets
  --tests -- -D warnings
- cargo clippy -p warp_completer --all-targets --tests -- -D warnings
- cargo nextest run -p warp 'ai::agent::conversation::tests::'
  (31/31 passed)

Proto dependency: warpdotdev/warp-proto-apis#315 (HEAD d622e1c)

Co-Authored-By: Oz <oz-agent@warp.dev>
Implements §8 of the QUALITY-780 client TECH spec:

- §8.1 detection: BlocklistAIHistoryModel.apply_client_actions now
  pattern-matches Message::ToolCall::WaitForEvents in inbound
  AddMessagesToTask actions and transitions the owning conversation to
  WaitingForEvents via the new mark_conversation_waiting_for_events
  helper. The in-memory unresolved tool_call_id is stored on the
  AIConversation so the later resume signal can be matched against it
  (the id is intentionally NOT persisted; restart hydrates to None and
  the post-restart fallback scans the transcript via
  find_unresolved_wait_for_events_tool_call_id).

- §8.3 resume detection: the same apply_client_actions hook also
  pattern-matches Message::ToolCallResult.WaitForEvents (server echo of
  the watchdog-timeout result) and Message::ToolCallResult.Cancel (the
  generic supersede-by-inbound-input marker), routing both through
  clear_conversation_waiting_for_events_if_matches so only the
  unresolved tool_call_id collapses the waiting state.

- §8.2 ordering rule: the BlocklistAIController action subscriber that
  fires the post-stream Success transition now skips the
  update_conversation_status(Success) call when the conversation is
  already in WaitingForEvents, so the detection transition isn't
  clobbered by the response-stream completion path. The response
  stream itself is still marked completed successfully via
  mark_response_stream_completed_successfully.

- §4 emission helper (scope-expansion from orchestrator): adds a
  public submit_wait_for_events_timeout(conversation_id, tool_call_id,
  ctx) on BlocklistAIController that the driver wave will invoke from
  its local watchdog when the idle timer fires. The helper performs
  the WaitingForEvents → InProgress transition immediately via
  clear_conversation_waiting_for_events_if_matches; the wire form
  (Request.Input.ToolCallResult.WaitForEvents) is emitted on the next
  outbound request via the new TryFrom<WaitForEventsResult> proto
  conversion.

- Adds AIAgentActionResultType::WaitForEvents(WaitForEventsResult)
  with Display / description / is_successful / redaction / SDK-output
  arms. Wires Wave 0 proto SHA bump d622e1cec8de5c50b55c2d4f3050ce48dae464c4
  (Request.Input.ToolCallResult.WaitForEvents variant + top-level
  WaitForEventsResult).

- Cleans up TODO(client-detection) markers in convert_conversation.rs:
  both convert_tool_call_result_to_input and create_cancelled_result_
  for_tool_call return None for WaitForEvents because the
  wait-state transition lives in apply_client_actions, not on the
  exchange input stream — restored transcripts stay symmetric with
  the Server variant.

Tests: history_model_tests covers mark/clear helpers, mismatch noop,
unconditional clear, and new-conversation no-inheritance (PRODUCT.md
(31)).

Co-Authored-By: Oz <oz-agent@warp.dev>
…integration)

Replaces the client-driver Wave 2 `TODO(integration)` stub in `emit_wait_for_events_timeout_result` with the actual call into

client-detection's `BlocklistAIController::submit_wait_for_events_timeout` helper. Resolves the cross-agent boundary at

integration time per the orchestration plan.

Co-Authored-By: Oz <oz-agent@warp.dev>
Adds api::ToolType::WaitForEvents to the orchestration block of get_supported_tools so the server can detect that this client build can pattern-match the new public wait_for_events tool-call variant. Without this declaration, the server's new gating logic falls back to the legacy server-handled emission to protect older clients.

Co-Authored-By: Oz <oz-agent@warp.dev>
Adds an explicit `Tool::WaitForEvents` arm to the action-conversion match next to `Tool::Server`. Without it, the wildcard fall-through raised `ToolToAIAgentActionError::UnexpectedTool` whenever `apply_client_actions` processed a `WaitForEvents` tool-call message, surfacing as "Failed to apply client actions to conversation: Conversation(UpdateTask(ConversionError(ToolError(UnexpectedTool))))" in the controller log.

`WaitForEvents` is handled separately via `BlocklistAIHistoryModel::mark_conversation_waiting_for_events` (client TECH §8.1), so the action-conversion path returns `MaybeAIAgentAction::NoClientRepresentation` \u2014 same pattern as the existing `Tool::Server` arm.

Co-Authored-By: Oz <oz-agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant