From 1a737d0bdad375930628ad1dce14e72bcfdc5caa Mon Sep 17 00:00:00 2001 From: Mason Daugherty <61371264+mdrxy@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:29:39 +0000 Subject: [PATCH 1/4] fix(code): keep chat input responsive during `/restart` `_handle_restart_command` awaited the multi-second `_restart_server_manual` (`server_proc.restart()`) directly on the Textual message pump, so key events stopped being forwarded and the chat input froze until the restart finished. Run the respawn as a detached `asyncio.create_task` (mirroring the MCP viewer/force-reconnect paths) so the pump stays free and typing lands while the restart is in-flight. Co-authored-by: open-swe[bot] --- libs/code/deepagents_code/app.py | 33 +++++++++++++ libs/code/tests/unit_tests/test_app.py | 68 ++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 8b6ca64a78..678c3bd092 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -2721,6 +2721,14 @@ def __init__( self._active_mcp_viewer: Any = None """Handle to the `/mcp` modal so server-ready events can refresh it.""" + self._restart_respawn_task: asyncio.Task[None] | None = None + """Strong reference to the detached `/restart` respawn task. + + `_handle_restart_command` runs the multi-second server respawn off the + Textual message pump via `asyncio.create_task` so the chat input stays + responsive; holding the reference keeps the task from being GC'd + mid-flight and lets tests await it deterministically.""" + self._pending_mcp_reconnect: bool = False """Set after a successful MCP login when the user defers the server restart. Cleared by the next reconnect or restart so multiple deferred @@ -19026,6 +19034,31 @@ async def _handle_restart_command(self, command: str) -> None: ) return + # Run the respawn as a detached task, NOT awaited on the message pump. + # `_respawn_server`'s multi-second `server_proc.restart()` would + # otherwise stall the pump — key events stop being forwarded and the + # chat input freezes ("blocked") for the whole restart. Mirrors the + # MCP viewer/force-reconnect paths: `asyncio.create_task` keeps the + # pump free, so keystrokes stay live and any message the user submits + # while `_connecting` is queued and drained once `ServerReady` fires. + # `_run_restart_respawn` owns the transient status and completion + # banner; `_log_task_exception` surfaces anything unexpected. The + # pre-respawn guards above (remote/starting/failed/deferred) already + # ran synchronously, so the user got immediate feedback before this. + task = asyncio.create_task(self._run_restart_respawn()) + self._restart_respawn_task = task + task.add_done_callback(_log_task_exception) + + async def _run_restart_respawn(self) -> None: + """Respawn the server for `/restart`, detached from the message pump. + + Scheduled via `asyncio.create_task` from `_handle_restart_command` so + the multi-second `server_proc.restart()` runs off the Textual message + pump, keeping the chat input responsive. Shows a transient + "Restarting server..." status for the duration, removes it whether the + respawn succeeds, returns `False`, or raises, and mounts the + completion banner only on success. + """ restarting = await self._mount_transient_app_message("Restarting server...") restarted = False try: diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index ef8df08160..701550968b 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -24186,6 +24186,10 @@ async def _fake_restart() -> bool: # noqa: RUF029 # awaited by handler monkeypatch.setattr(app, "_restart_server_manual", _fake_restart) await app._handle_command("/restart") + # The respawn runs as a detached task so the message pump stays + # free; await it to observe the completion banner deterministically. + assert app._restart_respawn_task is not None + await app._restart_respawn_task await pilot.pause() assert reload_called @@ -24197,12 +24201,63 @@ async def _fake_restart() -> bool: # noqa: RUF029 # awaited by handler assert not any("Restarting server" in m for m in app_msgs) assert any("Restart complete" in m for m in app_msgs) + @pytest.mark.timeout(15) + async def test_restart_keeps_chat_input_responsive( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """`/restart` must not freeze the chat input while the server respawns. + + Regression guard: `_handle_restart_command` used to await the + multi-second `_restart_server_manual()` (i.e. `server_proc.restart()`) + directly on the Textual message pump, so key events could not be + forwarded and the chat input was blocked until the restart finished. + Running the respawn as a detached task keeps the pump free, so typing + lands even while the restart is in-flight. Without the fix this test + times out (pump stalled on the gated coroutine). + """ + app = DeepAgentsApp(agent=MagicMock()) + async with app.run_test() as pilot: + await pilot.pause() + assert app._chat_input is not None + app._chat_input.focus_input() + await pilot.pause() + + app._server_proc = MagicMock() + app._server_kwargs = {} + + gate = asyncio.Event() + + async def _blocked_restart() -> bool: + await gate.wait() + return True + + from deepagents_code.config import settings + + monkeypatch.setattr(settings, "reload_from_environment", list) + monkeypatch.setattr( + "deepagents_code.model_config.clear_caches", lambda: None + ) + monkeypatch.setattr(app, "_restart_server_manual", _blocked_restart) + + await app._handle_command("/restart") + await pilot.pause() + # The respawn is now in-flight and gated open; the pump must still + # deliver keystrokes to the chat input. + await pilot.press("h", "i") + await pilot.pause() + typed = app._chat_input.value + gate.set() + assert app._restart_respawn_task is not None + await app._restart_respawn_task + await pilot.pause() + assert typed == "hi" + async def test_failed_restart_removes_transient_and_suppresses_completion( self, monkeypatch: pytest.MonkeyPatch ) -> None: """A failed restart removes "Restarting..." without showing completion. - Guards the conditional gate in `_handle_restart_command`: the success + Guards the conditional gate in `_run_restart_respawn`: the success banner is only mounted when `_restart_server_manual()` returns `True`. On failure the recovery UI (via `ServerStartFailed`) is the user's feedback, so stale transient progress and the misleading completion @@ -24234,6 +24289,8 @@ async def _fake_restart() -> bool: # noqa: RUF029 # awaited by handler monkeypatch.setattr(app, "_restart_server_manual", _fake_restart) await app._handle_command("/restart") + assert app._restart_respawn_task is not None + await app._restart_respawn_task await pilot.pause() assert restart_called @@ -24248,10 +24305,11 @@ async def test_raising_restart_removes_transient_and_propagates( The transient "Restarting server..." status is mounted before `_restart_server_manual()` is awaited, so the `try/finally` in - `_handle_restart_command` exists solely to remove it when the restart + `_run_restart_respawn` exists solely to remove it when the restart raises (not merely returns `False`). On a raise the transient must be gone, the misleading completion banner must never mount, and the - exception must propagate rather than be swallowed. + exception must propagate out of the detached respawn task (surfaced via + `_log_task_exception`) rather than be swallowed. """ app = DeepAgentsApp() async with app.run_test() as pilot: @@ -24276,8 +24334,10 @@ async def _boom() -> bool: # noqa: RUF029 # awaited by handler ) monkeypatch.setattr(app, "_restart_server_manual", _boom) + await app._handle_restart_command("/restart") + assert app._restart_respawn_task is not None with pytest.raises(RuntimeError, match="respawn exploded"): - await app._handle_restart_command("/restart") + await app._restart_respawn_task await pilot.pause() assert restart_called From 63ac67f7a4b164e75be62bb4b2532ec65b54815b Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 17 Jul 2026 12:36:50 -0400 Subject: [PATCH 2/4] fix(code): gate input before detached `/restart` respawn --- libs/code/deepagents_code/app.py | 14 ++++++- libs/code/tests/unit_tests/test_app.py | 54 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 5ddeda7e38..5c098f32eb 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -19486,6 +19486,13 @@ async def _handle_restart_command(self, command: str) -> None: # banner; `_log_task_exception` surfaces anything unexpected. The # pre-respawn guards above (remote/starting/failed/deferred) already # ran synchronously, so the user got immediate feedback before this. + # Mark the app reconnecting before scheduling because `create_task` + # does not run the coroutine inline. Otherwise a submission or second + # `/restart` could enter before `_respawn_server` sets these fields. + self._connecting = True + self._reconnecting = True + self._agent = None + self._sync_status_connection() task = asyncio.create_task(self._run_restart_respawn()) self._restart_respawn_task = task task.add_done_callback(_log_task_exception) @@ -19500,11 +19507,16 @@ async def _run_restart_respawn(self) -> None: respawn succeeds, returns `False`, or raises, and mounts the completion banner only on success. """ - restarting = await self._mount_transient_app_message("Restarting server...") + restarting = None restarted = False try: + restarting = await self._mount_transient_app_message("Restarting server...") restarted = await self._restart_server_manual() finally: + if not restarted: + self._connecting = False + self._reconnecting = False + self._sync_status_connection() if restarting is not None: with suppress(NoMatches, ScreenStackError): await restarting.remove() diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 10e83c2e9f..3fa0c9895c 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -24268,6 +24268,60 @@ async def _blocked_restart() -> bool: await pilot.pause() assert typed == "hi" + @pytest.mark.timeout(15) + async def test_marks_reconnecting_before_detached_respawn_runs( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Restart state must be visible before the detached task's first await.""" + app = DeepAgentsApp(agent=MagicMock()) + async with app.run_test() as pilot: + await pilot.pause() + app._server_proc = MagicMock() + app._server_kwargs = {} + + status_started = asyncio.Event() + release_status = asyncio.Event() + + async def _blocked_status(_content: str) -> None: + status_started.set() + await release_status.wait() + + restart = AsyncMock(return_value=False) + + from deepagents_code.config import settings + + monkeypatch.setattr(settings, "reload_from_environment", list) + monkeypatch.setattr( + "deepagents_code.model_config.clear_caches", lambda: None + ) + monkeypatch.setattr(app, "_mount_transient_app_message", _blocked_status) + monkeypatch.setattr(app, "_restart_server_manual", restart) + + await app._handle_restart_command("/restart") + task = app._restart_respawn_task + assert task is not None + assert not status_started.is_set() + assert app._connecting is True + assert app._reconnecting is True + assert app._agent is None + + try: + await status_started.wait() + await app._submit_input("queued during restart", mode="normal") + assert len(app._pending_messages) == 1 + assert app._pending_messages[0].text == "queued during restart" + + await app._handle_restart_command("/restart") + assert app._restart_respawn_task is task + restart.assert_not_awaited() + finally: + release_status.set() + await task + + restart.assert_awaited_once() + assert app._connecting is False + assert app._reconnecting is False + async def test_failed_restart_removes_transient_and_suppresses_completion( self, monkeypatch: pytest.MonkeyPatch ) -> None: From 367d3374268bbf092b8b34f0c3615b14c7f42a3b Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 17 Jul 2026 14:09:13 -0400 Subject: [PATCH 3/4] fix(code): preserve queued prompts across server restarts --- libs/code/deepagents_code/app.py | 38 ++++++++- libs/code/tests/unit_tests/test_app.py | 107 ++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 5c098f32eb..7b12026d6e 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -19402,6 +19402,23 @@ async def _handle_restart_command(self, command: str) -> None: """ await self._mount_message(UserMessage(command)) + # A duplicate `/restart` bypasses the normal input queue while the + # first detached respawn is still connecting. Reject it before the + # destructive setup below so prompts queued during that respawn are + # preserved for the pending `ServerReady` handler to drain. + if ( + self._restart_respawn_task is not None + and self._connecting + and self._reconnecting + ): + await self._mount_message( + AppMessage( + "A server restart is already in progress. Queued prompts " + "will be sent once it finishes.", + ), + ) + return + # Sever in-flight work bound to the dying subprocess. `_cancel_worker` # discards the queued backlog too — those messages would otherwise # fire against the freshly respawned agent silently. This restart *is* @@ -19503,15 +19520,30 @@ async def _run_restart_respawn(self) -> None: Scheduled via `asyncio.create_task` from `_handle_restart_command` so the multi-second `server_proc.restart()` runs off the Textual message pump, keeping the chat input responsive. Shows a transient - "Restarting server..." status for the duration, removes it whether the - respawn succeeds, returns `False`, or raises, and mounts the - completion banner only on success. + "Restarting server..." status for the duration and removes it whether + the respawn succeeds, returns `False`, or raises. Mounts the completion + banner only on success; on any non-success outcome it clears the + `_connecting`/`_reconnecting` flags the caller pre-set (on success the + `ServerReady` handler clears them once the new server is live). + + An *unexpected* raise — distinct from the handled `return False` path, + which posts `ServerStartFailed` so the recovery UI gives the user + feedback — is caught here and surfaced as an `ErrorMessage`, mirroring + `_reconnect_from_viewer_safe`, which detaches the same respawn. Without + this the exception would reach only `_log_task_exception` and log a + warning the interactive user never sees; `_log_task_exception` stays a + last-resort backstop for anything that escapes even this handler. """ restarting = None restarted = False try: restarting = await self._mount_transient_app_message("Restarting server...") restarted = await self._restart_server_manual() + except Exception as exc: + logger.exception("Manual /restart of server raised unexpectedly") + await self._mount_message( + ErrorMessage(f"Restart failed: {type(exc).__name__}: {exc}"), + ) finally: if not restarted: self._connecting = False diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 3fa0c9895c..05ddaddda4 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -24272,7 +24272,11 @@ async def _blocked_restart() -> bool: async def test_marks_reconnecting_before_detached_respawn_runs( self, monkeypatch: pytest.MonkeyPatch ) -> None: - """Restart state must be visible before the detached task's first await.""" + """Restart state must be visible before the detached task's first await. + + A duplicate `/restart` must also observe that state and return without + discarding prompts queued while the first restart is still in flight. + """ app = DeepAgentsApp(agent=MagicMock()) async with app.run_test() as pilot: await pilot.pause() @@ -24314,6 +24318,8 @@ async def _blocked_status(_content: str) -> None: await app._handle_restart_command("/restart") assert app._restart_respawn_task is task restart.assert_not_awaited() + assert len(app._pending_messages) == 1 + assert app._pending_messages[0].text == "queued during restart" finally: release_status.set() await task @@ -24322,6 +24328,69 @@ async def _blocked_status(_content: str) -> None: assert app._connecting is False assert app._reconnecting is False + @pytest.mark.timeout(15) + async def test_message_queued_during_restart_drains_after_server_ready( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """A prompt queued mid-restart survives and drains once the server is up. + + Completes the "queued and drained once `ServerReady` fires" guarantee. + The detached respawn pre-marks `_connecting`, so a normal submission + during it is queued rather than run against the dying server. A + successful restart must neither discard nor drain that queue — the + flags stay set until `ServerReady` — and the session-start sequence the + `ServerReady` handler runs must then drain it. + """ + app = DeepAgentsApp(agent=MagicMock()) + async with app.run_test() as pilot: + await pilot.pause() + app._server_proc = MagicMock() + app._server_kwargs = {} + + gate = asyncio.Event() + + async def _gated_restart() -> bool: + await gate.wait() + return True + + from deepagents_code.config import settings + + monkeypatch.setattr(settings, "reload_from_environment", list) + monkeypatch.setattr( + "deepagents_code.model_config.clear_caches", lambda: None + ) + monkeypatch.setattr(app, "_restart_server_manual", _gated_restart) + + await app._handle_restart_command("/restart") + await pilot.pause() + # The detached respawn is in-flight with `_connecting` pre-marked, + # so a normal submission is queued, not run against the dying server. + await app._submit_input("queued during restart", mode="normal") + assert len(app._pending_messages) == 1 + + gate.set() + assert app._restart_respawn_task is not None + await app._restart_respawn_task + await pilot.pause() + + # A successful restart owns neither discarding nor draining the + # queue; the `ServerReady` handler drains, so the prompt is still + # pending here. + assert len(app._pending_messages) == 1 + assert app._pending_messages[0].text == "queued during restart" + + # Drive the drain the `ServerReady` handler performs (a respawn + # reconnect takes the `_initial_session_started` branch) and confirm + # the queued prompt is dispatched. + app._initial_session_started = True + drain_deferred = AsyncMock() + process_next = AsyncMock() + app._maybe_drain_deferred = drain_deferred # ty: ignore + app._process_next_from_queue = process_next # ty: ignore + await app._run_session_start_sequence() + drain_deferred.assert_awaited_once() + process_next.assert_awaited_once() + async def test_failed_restart_removes_transient_and_suppresses_completion( self, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -24368,18 +24437,23 @@ async def _fake_restart() -> bool: # noqa: RUF029 # awaited by handler assert not any("Restarting server" in m for m in app_msgs) assert not any("Restart complete" in m for m in app_msgs) - async def test_raising_restart_removes_transient_and_propagates( + async def test_raising_restart_surfaces_error_and_resets_state( self, monkeypatch: pytest.MonkeyPatch ) -> None: - """A raising `_restart_server_manual()` clears the transient, then raises. - - The transient "Restarting server..." status is mounted before - `_restart_server_manual()` is awaited, so the `try/finally` in - `_run_restart_respawn` exists solely to remove it when the restart - raises (not merely returns `False`). On a raise the transient must be - gone, the misleading completion banner must never mount, and the - exception must propagate out of the detached respawn task (surfaced via - `_log_task_exception`) rather than be swallowed. + """A raising `_restart_server_manual()` surfaces an error and un-wedges. + + This is the *unexpected* raise path, distinct from the handled + `return False` path (which posts `ServerStartFailed` for recovery). The + transient "Restarting server..." status is mounted before + `_restart_server_manual()` is awaited, and `_run_restart_respawn` runs + detached from the message pump, so the exception cannot propagate up a + command handler where Textual would surface it. Instead the respawn + wrapper must catch it (mirroring `_reconnect_from_viewer_safe`): the + transient must be gone, the misleading completion banner must never + mount, an `ErrorMessage` must surface the failure to the user rather + than it being logged-and-lost, and the pre-marked `_connecting`/ + `_reconnecting` flags must reset so input submission does not stay + wedged. The detached task itself completes without raising. """ app = DeepAgentsApp() async with app.run_test() as pilot: @@ -24406,14 +24480,21 @@ async def _boom() -> bool: # noqa: RUF029 # awaited by handler await app._handle_restart_command("/restart") assert app._restart_respawn_task is not None - with pytest.raises(RuntimeError, match="respawn exploded"): - await app._restart_respawn_task + # The wrapper catches the raise, so awaiting the task does not + # re-raise — the failure surfaces as an `ErrorMessage` instead. + await app._restart_respawn_task await pilot.pause() assert restart_called app_msgs = [str(w._content) for w in app.query(AppMessage)] assert not any("Restarting server" in m for m in app_msgs) assert not any("Restart complete" in m for m in app_msgs) + errors = [str(w._content) for w in app.query(ErrorMessage)] + assert any("respawn exploded" in m for m in errors) + # The pre-marked reconnecting state must be cleared so a live input + # is not left permanently gated by `_connecting`. + assert app._connecting is False + assert app._reconnecting is False async def test_reload_failure_skips_restart( self, monkeypatch: pytest.MonkeyPatch From fc0307e880c2cfc62089c9b26d14306225b38b2f Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 17 Jul 2026 15:04:51 -0400 Subject: [PATCH 4/4] test(code): avoid startup race in forced sync tests --- libs/code/tests/unit_tests/test_app.py | 90 ++++++++++++-------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 05ddaddda4..bd746c47bb 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -8849,59 +8849,55 @@ async def test_forced_sync_retries_transient_read_failure(self) -> None: async def test_forced_sync_create_double_fault_prompts_retry(self) -> None: """A create with no local pending state prompts the user to retry.""" app = DeepAgentsApp(agent=MagicMock()) - async with app.run_test() as pilot: - await pilot.pause() - app._lc_thread_id = "thread-1" - app._pending_goal_objective = None - app._pending_goal_rubric = None - mount = AsyncMock() - with ( - patch("deepagents_code.app._GOAL_SYNC_READ_RETRY_SECONDS", 0), - patch.object( - app, - "_get_thread_state_values", - AsyncMock(side_effect=RuntimeError("down")), - ), - patch.object(app, "_mount_message", mount), - patch.object(app, "notify"), - ): - await app._sync_goal_rubric_state_from_thread(force=True) + app._lc_thread_id = "thread-1" + app._pending_goal_objective = None + app._pending_goal_rubric = None + mount = AsyncMock() + with ( + patch("deepagents_code.app._GOAL_SYNC_READ_RETRY_SECONDS", 0), + patch.object( + app, + "_get_thread_state_values", + AsyncMock(side_effect=RuntimeError("down")), + ), + patch.object(app, "_mount_message", mount), + patch.object(app, "notify"), + ): + await app._sync_goal_rubric_state_from_thread(force=True) - mount.assert_awaited_once() - await_args = mount.await_args - assert await_args is not None - body = str(await_args.args[0]._content) - assert "could not be loaded" in body + mount.assert_awaited_once() + await_args = mount.await_args + assert await_args is not None + body = str(await_args.args[0]._content) + assert "could not be loaded" in body async def test_forced_sync_amend_double_fault_remounts_review(self) -> None: """An amend keeps its local pending proposal, so the review remounts.""" app = DeepAgentsApp(agent=MagicMock()) - async with app.run_test() as pilot: - await pilot.pause() - app._lc_thread_id = "thread-1" - app._pending_goal_objective = "ship login" - app._pending_goal_rubric = "- passkeys work" - app._pending_goal_request_id = "request-amend" - remount = AsyncMock() - mount = AsyncMock() - with ( - patch("deepagents_code.app._GOAL_SYNC_READ_RETRY_SECONDS", 0), - patch.object( - app, - "_get_thread_state_values", - AsyncMock(side_effect=RuntimeError("down")), - ), - patch.object(app, "_remount_pending_goal_rubric_review", remount), - patch.object(app, "_mount_message", mount), - patch.object(app, "notify"), - ): - await app._sync_goal_rubric_state_from_thread( - force=True, - proposal_request_id="request-amend", - ) + app._lc_thread_id = "thread-1" + app._pending_goal_objective = "ship login" + app._pending_goal_rubric = "- passkeys work" + app._pending_goal_request_id = "request-amend" + remount = AsyncMock() + mount = AsyncMock() + with ( + patch("deepagents_code.app._GOAL_SYNC_READ_RETRY_SECONDS", 0), + patch.object( + app, + "_get_thread_state_values", + AsyncMock(side_effect=RuntimeError("down")), + ), + patch.object(app, "_remount_pending_goal_rubric_review", remount), + patch.object(app, "_mount_message", mount), + patch.object(app, "notify"), + ): + await app._sync_goal_rubric_state_from_thread( + force=True, + proposal_request_id="request-amend", + ) - remount.assert_awaited_once() - mount.assert_not_awaited() + remount.assert_awaited_once() + mount.assert_not_awaited() async def test_fetch_thread_history_coerces_unknown_goal_status(self) -> None: """Loading a thread with an unknown status drops it to None."""