Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions coding-agent/src/coding_agent/server/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2594,21 +2594,7 @@ async def update_runtime_config(
if getattr(session, "turn_in_progress", False):
raise HTTPException(status_code=409, detail="Turn already in progress")

# Apply thinking and approval directly on live mutable session/runtime state.
# No pipeline rebuild needed for these: provider reads thinking per-turn from
# ctx.config, and approval is evaluated by the existing live plugin.
no_rebuild_update = not (provided & {"model", "provider", "base_url"})
if body.thinking is not None:
session.thinking_config["enabled"] = body.thinking.enabled
session.thinking_config["effort"] = body.thinking.effort
if body.approval is not None:
approval_policy = ApprovalPolicy(body.approval)
plugin = _live_approval_plugin(session)
if plugin is not None:
plugin.set_policy(approval_policy)
session.approval_policy = approval_policy
await session_manager._persist_session_async(session)

try:
if no_rebuild_update:
updated_session = session
Expand All @@ -2634,6 +2620,20 @@ async def update_runtime_config(
status_code=500, detail=_http_exception_detail(exc)
) from exc

# Apply thinking and approval after the fallible rebuild. For rebuilds this
# targets the freshly attached runtime; for approval/thinking-only updates it
# keeps the existing no-rebuild fast path.
if body.thinking is not None:
updated_session.thinking_config["enabled"] = body.thinking.enabled
updated_session.thinking_config["effort"] = body.thinking.effort
if body.approval is not None:
approval_policy = ApprovalPolicy(body.approval)
plugin = _live_approval_plugin(updated_session)
if plugin is not None:
plugin.set_policy(approval_policy)
updated_session.approval_policy = approval_policy
await session_manager._persist_session_async(updated_session)

return RuntimeConfigUpdateResponse(
session_id=session_id,
provider_name=getattr(updated_session, "provider_name", None),
Expand Down
58 changes: 58 additions & 0 deletions coding-agent/tests/ui/test_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2444,6 +2444,64 @@ async def fail_replace_session_runtime_config(*args, **kwargs):
Approve,
)

async def test_mixed_runtime_replacement_failure_leaves_approval_and_thinking_unchanged(
self, client, monkeypatch
):
response = await client.post("/sessions", json={"approval_policy": "auto"})
session_id = response.json()["session_id"]
await session_manager.ensure_session_runtime(session_id)
session = session_manager.get_session(session_id)
original_thinking_config = dict(session.thinking_config)
persisted_before = session_manager._store.load(session_id)
assert persisted_before is not None
assert persisted_before["approval_policy"] == "auto"

plugin = session.runtime_pipeline._registry.get("approval")
assert isinstance(plugin, ApprovalPlugin)
assert isinstance(
plugin.approve_tool_call(tool_name="bash_run", arguments={}),
AskUser,
)

replace_calls: list[dict[str, object]] = []

async def fail_replace_session_runtime_config(*args, **kwargs):
replace_calls.append(dict(kwargs))
raise RuntimeError("runtime replacement failed")

monkeypatch.setattr(
session_manager,
"replace_session_runtime_config",
fail_replace_session_runtime_config,
)

update = await client.post(
f"/sessions/{session_id}/runtime-config",
json={
"approval": "yolo",
"thinking": {"enabled": False, "effort": "high"},
"model": "replacement-model",
},
)

assert update.status_code == 500
assert replace_calls == [
{
"model_name": "replacement-model",
"provider_name": None,
"base_url": http_server.UNSET,
}
]
assert session.approval_policy == ApprovalPolicy.AUTO
assert session.thinking_config == original_thinking_config
assert isinstance(
plugin.approve_tool_call(tool_name="bash_run", arguments={}),
AskUser,
)
persisted_after = session_manager._store.load(session_id)
assert persisted_after is not None
assert persisted_after["approval_policy"] == "auto"

async def test_approval_null_is_rejected(self, client):
response = await client.post("/sessions", json={})
session_id = response.json()["session_id"]
Expand Down
Loading