fix(code): clear stale live approval mode keys (#4242)

When a user turns live approval mode on or off, the app keeps a session
key for the live approval-mode state so it can update or clean up that
state later. If the current agent cannot provide that live state writer,
the app should treat the write as failed and forget any old key instead
of holding on to stale state.

The user story is: a user toggles live approval mode while a session is
active, but the active agent is one that can run without writing to the
shared live approval-mode store. That can happen with local or
in-process agents, older/custom agent wrappers, test doubles, or
partially initialized agents that do not expose the remote store writer
used for live approval-mode updates. In that situation, keeping an old
key would make the app think it still has a valid live approval-mode
record even though the current agent cannot update it.

This change makes the no-writer case fail safely. The app clears the
stale key, reports the write as unsuccessful, and falls back to the
existing approval behavior instead of carrying an outdated live
approval-mode key into later tool approval checks.
This commit is contained in:
Mason Daugherty
2026-06-25 00:20:16 -04:00
committed by GitHub
parent 6c930c5e45
commit f11a76962c
2 changed files with 49 additions and 0 deletions
+8
View File
@@ -5576,6 +5576,14 @@ class DeepAgentsApp(App):
self._session_state.approval_mode_key = None
logger.warning("Failed to write live approval-mode state", exc_info=True)
return False
if live_key is None:
# No store writer on the agent (a local/in-process agent rather
# than a RemoteAgent). This is an expected configuration, not a
# fault, so — unlike the except branch above — we clear the stale
# key and fail closed without logging, to avoid noise on every
# toggle. The run-context path persists the mode for local agents.
self._session_state.approval_mode_key = None
return False
self._session_state.approval_mode_key = live_key
return True
+41
View File
@@ -13330,6 +13330,18 @@ class TestLiveApprovalModeWrites:
assert not await app._write_live_approval_mode()
assert app._session_state.approval_mode_key is None
async def test_write_live_approval_mode_fails_without_writer(self) -> None:
app = DeepAgentsApp()
app._agent = object()
app._session_state = TextualSessionState(
thread_id="thread-1",
auto_approve=False,
)
app._session_state.approval_mode_key = "stale"
assert not await app._write_live_approval_mode()
assert app._session_state.approval_mode_key is None
async def test_toggle_off_failed_write_cancels_running_agent(self) -> None:
app = DeepAgentsApp(auto_approve=True)
async with app.run_test() as pilot:
@@ -13358,6 +13370,35 @@ class TestLiveApprovalModeWrites:
notify.assert_called_once()
assert notify.call_args.kwargs["severity"] == "warning"
async def test_toggle_off_no_writer_cancels_running_agent(self) -> None:
# Unlike the test above, this drives a *real* writer-less agent
# (no `aput_store_item`) so the False return originates from the
# `live_key is None` branch rather than a mock, proving that the
# no-writer condition actually reaches the mid-run cancel path.
app = DeepAgentsApp(auto_approve=True)
async with app.run_test() as pilot:
await pilot.pause()
app._agent = object()
app._session_state = TextualSessionState(
thread_id="thread-1",
auto_approve=True,
)
app._session_state.approval_mode_key = "stale"
app._agent_running = True
with (
patch.object(app, "_force_interrupt_active_work") as force,
patch.object(app, "notify") as notify,
):
await app.action_toggle_auto_approve()
assert app._auto_approve is False
assert app._session_state.auto_approve is False
assert app._session_state.approval_mode_key is None
force.assert_called_once()
notify.assert_called_once()
assert notify.call_args.kwargs["severity"] == "warning"
assert "cancelled for safety" in notify.call_args.args[0]
async def test_toggle_off_failed_write_does_not_cancel_when_idle(self) -> None:
app = DeepAgentsApp(auto_approve=True)
async with app.run_test() as pilot: