Skip to content

Fix local Oz run_agents child rendering as duplicate local + remote pills - #15583

Merged
cephalonaut merged 7 commits into
masterfrom
factory/quality-1897-local-oz-dual-pill
Aug 28, 2026
Merged

Fix local Oz run_agents child rendering as duplicate local + remote pills#15583
cephalonaut merged 7 commits into
masterfrom
factory/quality-1897-local-oz-dual-pill

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

One local run_agents dispatch (local: {}, single child, default Oz harness) could render two orchestration pills for the same server run — one local, one remote-flagged — while the server had only one child run and one conversation. The identical omission existed in the TUI front-end, filed as QUALITY-1902 and fixed here too.

Root cause, in both front-ends (warp-server's EventChildAgentStarted emission for local children is correct and untouched): the local-Oz child launch path stamped the child's task id with a bare conversation.set_task_id(...), which never updates the agent_id_to_conversation_id index — the only idempotency key the SSE remote-child-placeholder path (ensure_remote_child_placeholder / finish_remote_child_placeholder) checks. Whichever of the two async paths — the local launch's own callback, or the SSE placeholder's task-metadata fetch — resolved second created a duplicate, remote-flagged conversation for the same run id.

Changes

The fix closes both race orderings with two changes:

  1. Index the run id at launch. Both local launch paths now call assign_run_id_for_conversation instead of the bare set_task_id:
    • GUI: launch_local_no_harness_child (app/src/pane_group/pane/terminal_pane.rs) via a new shared helper, finish_local_oz_child_conversation (app/src/ai/blocklist/child_agent_launch.rs).
    • TUI: register_local_oz_child_session (crates/warp_tui/src/orchestration_model.rs) calls the same shared helper, re-exported via tui_export.rs.
  2. Reconcile a raced placeholder inside the index writer itself. assign_run_id_for_conversation (app/src/ai/blocklist/history_model.rs) now checks whether the run id is already mapped to a different conversation; if that conversation is_remote_child(), it's a stale SSE placeholder and gets discarded (discard_stale_placeholder_for_run_id); otherwise the mismatch is logged rather than silently deleted, since that would indicate an unrelated bug. Credit: this centralized, placeholder-gated design is adopted from Matthew Albright's independent fix in #15584, including its regression test.

(1) alone would still leave the reverse race open (SSE placeholder wins first); (2) alone is unreachable on the no-harness path, since a launch path that never calls assign_run_id_for_conversation never reaches the guard, and the SSE path's own pre-existing idempotency check (conversation_id_for_agent_id) never resolves either, since the run id was never indexed. Both are required together.

The TUI's tab bar does not currently render the resulting duplicate conversation (TuiOrchestrationModel::snapshot filters children by TUI session presence, and a placeholder has no session), so this closes a latent data-model inconsistency there rather than an observed visible symptom.

Known remaining gap, not fixed here: initialize_output_for_response_stream (history_model.rs, ~1504) also writes agent_id_to_conversation_id directly and doesn't go through the new guard. For this fix's case that's harmless (assign_run_id_for_conversation already runs first, at launch time), but it's an architecturally unguarded writer of the same index for other call paths. Filed as follow-up hardening: QUALITY-1911.

Linked Issue

Fixes QUALITY-1897 and QUALITY-1902.

  • Diagnosis and fix direction were provided directly on the issue.
  • Visual verification captured for the GUI flow — see below.

Testing

  • local_oz_launch_indexes_run_id_before_child_agent_started_sse (GUI, orchestration_event_streamer_tests.rs) — drives finish_local_oz_child_conversation directly (the exact call launch_local_no_harness_child makes), so a regression there fails the test.
  • sse_placeholder_then_local_launch_converges_on_one_conversation (GUI) — SSE placeholder created first, then the local launch reclaims the run id through the guard; asserts exactly one conversation survives.
  • primary_placeholder_still_created_for_out_of_band_local_children (GUI) — an out-of-band LOCAL child (dispatched via CLI/API on another device) still gets a placeholder, guarding against over-broadly suppressing legitimate placeholders.
  • assign_run_id_for_conversation_discards_stale_remote_placeholder_for_same_run_id (GUI, adopted from Fix duplicate orchestration pills for local child agents #15584) — unit coverage for the guard itself.
  • local_oz_child_session_indexes_run_id_immediately (TUI, orchestration_model_tests.rs) — drives register_local_oz_child_session itself with a real MaterializedLocalOzChildSession, so a regression at that call site fails the test.

Each of the tests above that targets a specific call site was verified by reverting that production line, confirming the test failed, then restoring it.

Ran:

  • cargo nextest run -p warp (full package): 6257/6260 passed. The 3 failures (missing nsc tool; a multibyte-decoration test; a zsh histignorespace test) are pre-existing on master too (confirmed via git stash + re-run), unrelated to this change.
  • cargo nextest run -p warp_tui -p warp --lib -E 'package(warp_tui) and test(orchestration_model)': 22/22 passed. (Building warp_tui in full isolation hits a pre-existing, unrelated warpui platform-delegate compile gap under the test-util feature; building it alongside warp resolves features the way the real workspace build does and avoids it.)
  • ./script/format and cargo clippy -p warp --all-targets --all-features --tests -- -D warnings / cargo clippy -p warp_tui -p warp --lib --tests -- -D warnings — clean, no new diagnostics.

Visual verification

This is a background race, not reachable through a deterministic manual repro. A delegated computer-use session built this branch, launched the real Warp GUI, and drove an actual run_agents(local: {...}) dispatch end to end. Result: exactly one pill rendered for the local child, labeled correctly as local, ran to completion.

Agent Mode

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

CHANGELOG-BUG-FIX: Fixed a race that could make a single local Oz run_agents child appear as two orchestration pills.

…ills

One local run_agents dispatch could produce two orchestration pills for a
single agent run: a local one and a remote-flagged one, both pointing at
the same server run_id.

Root causes, all client-side:
1. launch_local_no_harness_child only stamped task_id on the local
   conversation via set_task_id, which never updates the
   agent_id_to_conversation_id index used as the SSE placeholder's
   idempotency key. Now it calls assign_run_id_for_conversation, mirroring
   launch_local_harness_child.
2. finish_remote_child_placeholder treated every child_agent_started event
   as an out-of-band remote child, with no exemption for children the
   owning (Primary) client will register in-band. It now skips creating a
   placeholder when the fetched task reports execution_location = LOCAL
   under Primary drain mode; Observer (shared-session viewer) placeholders
   are unaffected since they have no in-band counterpart.
3. Both local launch paths now remove any existing conversation already
   indexed under the run id before creating their own, closing the reverse
   race where the SSE placeholder's fetch resolves before the local pane
   exists.

Fixes QUALITY-1897.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation View on Slack

…-index test hold the fix, trim comments

- finish_remote_child_placeholder: remove the execution_location == LOCAL
  skip. It incorrectly suppressed out-of-band LOCAL children (dispatched
  via CLI/API, executing on another device) that this process never
  launches in-band. The pre-existing conversation_id_for_agent_id
  re-check plus the new remove_existing_conversation_for_run_id call at
  both local launch sites already converge both race orderings to one
  conversation without needing to special-case execution_location.
- Extracted finish_local_oz_child_conversation (child_agent_launch.rs) so
  launch_local_no_harness_child's run-id indexing is a named, directly
  testable call. local_oz_launch_indexes_run_id_before_child_agent_started_sse
  now drives that exact helper instead of hand-rolling the equivalent
  history-model calls, so a regression in the helper fails the test
  (verified locally by reverting it to set_task_id and confirming the
  test fails).
- Added primary_placeholder_still_created_for_out_of_band_local_children
  covering the Primary + out-of-band-LOCAL case; removed the two tests
  that only exercised the now-removed execution_location branch; updated
  sse_placeholder_then_local_launch_converges_on_one_conversation for the
  new (no-skip) behavior.
- Trimmed transformation/cross-reference comments per review.
…rt (QUALITY-1902)

- Move the raced-placeholder discard from per-call-site
  remove_existing_conversation_for_run_id into a centralized guard,
  discard_stale_placeholder_for_run_id, invoked from inside
  assign_run_id_for_conversation itself (credit: PR #15584 by the
  requester). This guard only discards an existing mapping when it is a
  remote-child placeholder (is_remote_child); a non-placeholder mapping
  is left in place and logged, since that would indicate an unrelated
  bug rather than a race to resolve. Because both local launch paths
  already call assign_run_id_for_conversation (the no-harness path via
  finish_local_oz_child_conversation, added earlier in this PR), no
  launch site needs its own explicit cleanup call anymore; removed
  remove_existing_conversation_for_run_id and its call sites.
- Adopted PR #15584's regression test verifying the centralized guard
  directly, and updated the local-launch composition test to rely on
  it transitively through finish_local_oz_child_conversation instead of
  calling the (now removed) explicit helper.
- Fixed the TUI counterpart of the same bug (QUALITY-1902):
  register_local_oz_child_session in
  crates/warp_tui/src/orchestration_model.rs had the identical
  set_task_id-only omission. It now calls finish_local_oz_child_conversation,
  the same shared helper the GUI path uses (re-exported via
  tui_export.rs), and gets the centralized discard guard for free. Added
  a mirroring regression test, local_oz_child_session_indexes_run_id_immediately.
…test

The previous version pre-created the conversation via the add_child_session
test helper and called finish_local_oz_child_conversation directly,
bypassing register_local_oz_child_session entirely -- the same mistake
already caught and fixed once on the GUI side (finding 2 in the first
review pass). Reverting the production call site inside
register_local_oz_child_session back to set_task_id left the test green.

Now constructs a real MaterializedLocalOzChildSession (with a lightweight
test terminal session/view, no real PTY) and calls
register_local_oz_child_session itself, then asserts the run id resolves
via the parent's single child conversation. Verified by reverting the
call site, confirming the test failed, then restoring it.
@cephalonaut
cephalonaut marked this pull request as ready for review August 27, 2026 01:28
Comment thread app/src/ai/blocklist/history_model.rs Outdated
@warp-local-for-testing-only

Copy link
Copy Markdown

An unexpected error has occurred: managed MCP server 01a01a21-3ec9-71a1-99f2-f13b1773c920 is not active

The server emits child_agent_started on the parent for every child,
local ones included, so the SSE family drain cannot tell an in-band
child apart from one spawned out-of-band (CLI/API) purely from the
event -- that's why a locally spawned child can still get an
is_remote_child placeholder materialized for it. Add that clause so
the comment doesn't just name the mechanism without the premise that
makes it necessary.
…local-oz-dual-pill

# Conflicts:
#	app/src/pane_group/pane/terminal_pane.rs
#	crates/warp_tui/src/orchestration_model.rs
@cephalonaut
cephalonaut enabled auto-merge (squash) August 28, 2026 04:14
@cephalonaut
cephalonaut merged commit 061318f into master Aug 28, 2026
26 checks passed
@cephalonaut
cephalonaut deleted the factory/quality-1897-local-oz-dual-pill branch August 28, 2026 04:35
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.

2 participants