From e8b2dfda369df7c77df65ab2e893283232fdad09 Mon Sep 17 00:00:00 2001 From: Alexander Miroshnichenko Date: Thu, 20 Aug 2026 13:38:39 +0300 Subject: [PATCH] fix(haivemind): query MainScreen instance directly instead of app default screen App.query_one searches the app's default screen (_default), but #chat-log/#health-bar/etc. live on the pushed MainScreen. Every HaivemindApp handler failed with NoMatches at runtime. Hold a reference to the MainScreen instance and query it directly so widget lookup no longer depends on which screen is active. --- packages/haivemind/fix-chat-log-race.patch | 454 ++++++++++++++++++++- 1 file changed, 438 insertions(+), 16 deletions(-) diff --git a/packages/haivemind/fix-chat-log-race.patch b/packages/haivemind/fix-chat-log-race.patch index 999f92f..423a7cd 100644 --- a/packages/haivemind/fix-chat-log-race.patch +++ b/packages/haivemind/fix-chat-log-race.patch @@ -1,23 +1,15 @@ --- a/haivemind_tui.py +++ b/haivemind_tui.py -@@ -1195,6 +1195,14 @@ - yield TheatrePanel(id="theatre-container") - yield Footer() +@@ -1220,13 +1220,13 @@ + self._engine: HaivemindEngine | None = None + self._run_started = False + self._run_start_time: float = 0.0 ++ self._main_screen = None -+ def on_mount(self) -> None: -+ app = self.app -+ if ( -+ getattr(app, "_prompt", None) is not None -+ and getattr(app, "_config", None) is not None -+ ): -+ app.start_run(app._prompt, app._config) -+ - - class HaivemindApp(App): - CSS = CSS -@@ -1224,9 +1232,7 @@ def on_mount(self) -> None: - self.push_screen(MainScreen()) +- self.push_screen(MainScreen()) ++ self._main_screen = MainScreen() ++ self.push_screen(self._main_screen) self.set_interval(0.15, self._tick_spinner) - if self._prompt is not None and self._config is not None: - self.start_run(self._prompt, self._config) @@ -26,3 +18,433 @@ self.push_screen(ConfigScreen()) def start_run(self, prompt: str, config: HaivemindConfig) -> None: +@@ -1303,7 +1303,7 @@ + def _tick_spinner(self) -> None: + _advance_spinner() + try: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + if "streaming" in hbar._model_status.values(): + hbar._refresh() + except NoMatches: +@@ -1315,20 +1315,20 @@ + def _on_probe_start(self) -> None: + import time as _time + self._run_start_time = _time.time() +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary("[bold yellow]Probing models...[/]") + if self._config is not None: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_models(self._config.models) +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.consensus_info = "probing" + header.progress_pct = 0.0 + + @on(ProbeResult) + def _on_probe_result(self, event: ProbeResult) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "done" if event.ok else "failed") +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + if event.ok: + color = hbar.model_color(event.model) + chat.add_summary(f"[{color}][probe] ok: {event.model}[/]") +@@ -1339,44 +1339,44 @@ + + @on(ProbeDone) + def _on_probe_done(self, event: ProbeDone) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary( + f"[bold yellow]Probe complete:[/] {len(event.healthy)} healthy, " + f"{len(event.failures)} failed" + ) +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.consensus_info = "" + if event.healthy: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_models(event.healthy) +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.update_layout(event.healthy, hbar._model_colors) + + @on(RoundStart) + def _on_round_start(self, event: RoundStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary(f"\n[bold cyan]\u2500\u2500 Round {event.round_idx}/{event.total} \u2500\u2500[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = f"hAIvemind \u2502 Round {event.round_idx}/{event.total}" + header.consensus_info = "" + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.clear_all() + except NoMatches: + pass + + @on(ModelStart) + def _on_model_start(self, event: ModelStart) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + total = len(hbar._model_status) if hbar._model_status else 0 + done_count = sum( + 1 for s in hbar._model_status.values() if s in ("streaming", "done") + ) + header.model_info = f"Models: {done_count}/{total}" + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + active = [ + m for m, s in hbar._model_status.items() if s != "dropped" + ] +@@ -1386,27 +1386,27 @@ + + @on(ModelStream) + def _on_model_stream(self, event: ModelStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass + + @on(ModelDone) + def _on_model_done(self, event: ModelDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + ar = event.agent_round + if ar.error and ar.error not in ("stderr",): + hbar.set_status(event.model, "failed") + else: + hbar.set_status(event.model, "done") +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if ar.has_valid_draft: + chat.add_summary( +@@ -1419,14 +1419,14 @@ + + @on(ModelDropped) + def _on_model_dropped(self, event: ModelDropped) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "dropped") +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary( + f"[red]\u26a0 {event.model} dropped:[/] {event.reason}" + ) + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + remaining = [ + m for m, s in hbar._model_status.items() if s != "dropped" + ] +@@ -1436,7 +1436,7 @@ + + @on(RoundDone) + def _on_round_done(self, event: RoundDone) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + dur = event.record.get("duration_s", 0) + chat.add_summary( + f"[dim]\u2500\u2500 end round {event.round_idx} ({dur}s) \u2500\u2500[/]" +@@ -1446,15 +1446,15 @@ + best_sim = _compute_best_similarity(drafts) + threshold = self._config.threshold if self._config else 0.75 + pct = min(1.0, best_sim / threshold) if threshold > 0 else 0.0 +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.progress_pct = pct + header.consensus_info = f"sim: {best_sim:.2f}" + + @on(Consensus) + def _on_consensus(self, event: Consensus) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- header = self.query_one("#header-bar", HeaderBar) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + total_models = len(hbar._model_status) if hbar._model_status else 0 + import time as _time + elapsed = ( +@@ -1493,9 +1493,9 @@ + + @on(Fallback) + def _on_fallback(self, event: Fallback) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- header = self.query_one("#header-bar", HeaderBar) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + total_models = len(hbar._model_status) if hbar._model_status else 0 + header.consensus_info = "fallback" + header.progress_pct = 0.0 +@@ -1518,30 +1518,30 @@ + + @on(EngineError) + def _on_engine_error(self, event: EngineError) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary(f"[red]Error:[/] {event.message}") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.consensus_info = "error" + + @on(EngineLog) + def _on_engine_log(self, event: EngineLog) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + chat.add_summary(event.message) + + @on(MoaRefStart) + def _on_moa_ref_start(self, event: MoaRefStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") + color = hbar.model_color(event.model) + chat.add_summary(f"[{color}]\U0001f4e1 [ref] {event.model} analyzing...[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = "hAIvemind \u2502 MoA \u2502 References" + + @on(MoaRefDone) + def _on_moa_ref_done(self, event: MoaRefDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) +- chat = self.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if event.err and not event.raw.strip(): + hbar.set_status(event.model, "failed") +@@ -1552,32 +1552,32 @@ + + @on(MoaRefStream) + def _on_moa_ref_stream(self, event: MoaRefStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][ref:{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass + + @on(MoaAggStart) + def _on_moa_agg_start(self, event: MoaAggStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") + color = hbar.model_color(event.model) + chat.add_summary(f"\n[bold {color}]\U0001f9e0 [aggregator] {event.model} synthesizing...[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = "hAIvemind \u2502 MoA \u2502 Aggregating" + + @on(MoaAggDone) + def _on_moa_agg_done(self, event: MoaAggDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) +- chat = self.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if event.raw.strip(): + hbar.set_status(event.model, "done") +@@ -1588,14 +1588,14 @@ + + @on(MoaAggStream) + def _on_moa_agg_stream(self, event: MoaAggStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][agg:{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass +@@ -1604,18 +1604,18 @@ + + @on(FusionPanelStart) + def _on_fusion_panel_start(self, event: FusionPanelStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") + color = hbar.model_color(event.model) + chat.add_summary(f"[{color}]\U0001f52c [panel] {event.model} analyzing...[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = "hAIvemind \u2502 Fusion \u2502 Panel" + + @on(FusionPanelDone) + def _on_fusion_panel_done(self, event: FusionPanelDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) +- chat = self.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if event.err and not event.raw.strip(): + hbar.set_status(event.model, "failed") +@@ -1626,32 +1626,32 @@ + + @on(FusionPanelStream) + def _on_fusion_panel_stream(self, event: FusionPanelStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][panel:{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass + + @on(FusionJudgeStart) + def _on_fusion_judge_start(self, event: FusionJudgeStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") + color = hbar.model_color(event.model) + chat.add_summary(f"\n[bold {color}]\u2696\ufe0f [judge] {event.model} analyzing...[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = "hAIvemind \u2502 Fusion \u2502 Judging" + + @on(FusionJudgeDone) + def _on_fusion_judge_done(self, event: FusionJudgeDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) +- chat = self.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if event.raw.strip(): + hbar.set_status(event.model, "done") +@@ -1662,32 +1662,32 @@ + + @on(FusionJudgeStream) + def _on_fusion_judge_stream(self, event: FusionJudgeStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][judge:{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass + + @on(FusionSynthStart) + def _on_fusion_synth_start(self, event: FusionSynthStart) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + hbar.set_status(event.model, "streaming") + color = hbar.model_color(event.model) + chat.add_summary(f"\n[bold {color}]\u270d\ufe0f [synth] {event.model} writing final answer...[/]") +- header = self.query_one("#header-bar", HeaderBar) ++ header = self._main_screen.query_one("#header-bar", HeaderBar) + header.round_info = "hAIvemind \u2502 Fusion \u2502 Synthesizing" + + @on(FusionSynthDone) + def _on_fusion_synth_done(self, event: FusionSynthDone) -> None: +- hbar = self.query_one("#health-bar", ModelHealthBar) +- chat = self.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + color = hbar.model_color(event.model) + if event.raw.strip(): + hbar.set_status(event.model, "done") +@@ -1698,14 +1698,14 @@ + + @on(FusionSynthStream) + def _on_fusion_synth_stream(self, event: FusionSynthStream) -> None: +- chat = self.query_one("#chat-log", ChatLog) +- hbar = self.query_one("#health-bar", ModelHealthBar) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) ++ hbar = self._main_screen.query_one("#health-bar", ModelHealthBar) + color = hbar.model_color(event.model) + t = event.fragment.strip() + if t: + chat.add_summary(f"[{color}][synth:{event.model}][/] {t}") + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.stream_to_model(event.model, event.fragment) + except NoMatches: + pass +@@ -1720,7 +1720,7 @@ + + def action_toggle_theatre(self) -> None: + try: +- tc = self.query_one("#theatre-container") ++ tc = self._main_screen.query_one("#theatre-container") + if tc.has_class("visible"): + tc.remove_class("visible") + else: +@@ -1734,13 +1734,13 @@ + + def action_clear_theatre(self) -> None: + try: +- theatre = self.query_one("#theatre-container", TheatrePanel) ++ theatre = self._main_screen.query_one("#theatre-container", TheatrePanel) + theatre.clear_all() + except NoMatches: + pass + + def action_show_artifacts(self) -> None: +- chat = self.query_one("#chat-log", ChatLog) ++ chat = self._main_screen.query_one("#chat-log", ChatLog) + if self._engine and self._engine.run_dir: + chat.add_summary(f"[bold]Artifacts:[/] {self._engine.run_dir}") + else: