From eb7724d48fa5a559d15efc06280f1db870e8270b Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 4 Jul 2026 02:15:58 +0800 Subject: [PATCH] feat(orchestrator): drain the reveal sweep before finalize restructures finalize's ReplaceSubtree allocates ids the reveal overlay never saw, so a mid-animation section snapped in at once and the agent cursor lost its target. Wait (worker thread, abort-aware, capped) for the scheduled sweep to finish first. --- crates/op-orchestrator/src/run.rs | 8 ++++++++ crates/op-orchestrator/src/subagent.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/crates/op-orchestrator/src/run.rs b/crates/op-orchestrator/src/run.rs index a719fcc6e..7c533a5a9 100644 --- a/crates/op-orchestrator/src/run.rs +++ b/crates/op-orchestrator/src/run.rs @@ -542,6 +542,14 @@ impl Orchestrator { }); } + // Let the reveal sweep FINISH before cleanup restructures the tree: + // `finalize_design`'s ReplaceSubtree allocates fresh ids that were + // never registered with the reveal overlay, so a section still + // mid-animation snaps in all at once and the agent cursor loses its + // target (measured: the tail of a run popped in "一口气" while + // earlier sections streamed). Worker-thread wait, abort-aware. + crate::subagent::wait_for_reveal_drain(self.agent_indicator_epoch, abort); + // -- 阶段 4:清理 --(有内容才跑;空 root 无可清理) // Append mode (skip_root_insertion): scope cleanup to ONLY the roots // this run inserted (post-remap ids from each outcome) so pre-existing diff --git a/crates/op-orchestrator/src/subagent.rs b/crates/op-orchestrator/src/subagent.rs index 9e0929b81..07e86ddb6 100644 --- a/crates/op-orchestrator/src/subagent.rs +++ b/crates/op-orchestrator/src/subagent.rs @@ -332,6 +332,32 @@ pub(crate) async fn run_subtask_with_reveal_at( } } +/// Block (worker thread, abort-aware) until the reveal overlay's scheduled +/// sweep for `epoch` has finished playing. The finalize passes swap subtrees +/// via `ReplaceSubtree`, whose fresh ids were never registered with the +/// overlay — restructuring mid-sweep snaps the still-animating tail of the +/// design in at once and orphans the agent cursor. Capped so a stuck clock +/// can't hang the run. +pub(crate) fn wait_for_reveal_drain(epoch: Option, abort: &crate::types::AbortFlag) { + let Some(epoch) = epoch else { + return; + }; + let cap = reveal_now_millis().saturating_add(15_000); + loop { + if abort.is_set() { + return; + } + let now = reveal_now_millis(); + let Some(end) = op_editor_core::agent_indicators::latest_reveal_end_ms(epoch) else { + return; + }; + if now >= end || now >= cap { + return; + } + std::thread::sleep(std::time::Duration::from_millis((end - now).min(120))); + } +} + pub(crate) fn reveal_now_millis() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH)