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
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
id: TASK-12142
title: Fix audit ACP reconnect broadcaster cleanup
status: Done
assignee: []
created_date: 2026-07-04 06:55
updated_date: 2026-07-04 18:59
labels:
- audit
- remediation
- mcp
- acp
- websocket
- reliability
dependencies: []
references:
- AUDIT-2026-06-27-MCP-002
- https://github.com/rmusser01/tldw_server/pull/2619
documentation:
- Docs/superpowers/reviews/2026-06-27-repo-audit/domains/mcp-sandbox-agent-protocol.md
- Docs/superpowers/reviews/2026-06-27-repo-audit/specialists/reliability-lifecycle.md
priority: low
modified_files:
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py
- tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py
- tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py
- tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Address audit finding MCP-002: ACP reconnect WebSocket replay creates a temporary WSBroadcaster but the endpoint finalizer does not remove the replay connection or stop the broadcaster, leaving event-bus subscribers/tasks behind after disconnect.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Reconnect stream retains enough broadcaster lifecycle state for finalizer cleanup.
- [x] #2 Endpoint finalizer removes the replay connection and stops the temporary broadcaster after disconnect/error.
- [x] #3 Session event-bus subscriber count returns to baseline after last_sequence reconnect disconnect.
- [x] #4 WSBroadcaster supports unique consumer ids so concurrent reconnect replay broadcasters do not overwrite each other.
- [x] #5 Focused endpoint-level regression covers last_sequence reconnect cleanup.
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression coverage for endpoint reconnect-disconnect cleanup and broadcaster consumer-id isolation.
2. Make the reconnect broadcaster lifecycle explicit in acp_session_stream, including connection removal and stop in the finalizer.
3. Allow WSBroadcaster instances to use unique consumer ids while preserving the default consumer id for existing callers.
4. Run focused ACP websocket/broadcaster tests, Bandit on touched production scope, and diff whitespace validation.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Validation notes:
- Original PR branch was stale after latest dev advanced; fetched origin/dev and rebased cleanly onto fd5c152b065c408e4e8ee5f08da41589f21cb7f5. Post-rebase merge-base matched origin/dev before validation.
- Red check failed as expected before the original implementation: reconnect endpoint left `ws_broadcaster` in the session event bus and WSBroadcaster did not accept a custom `consumer_id`.
- Reviewed Gemini feedback on PR #2619. The `nonlocal` suggestion is technically invalid for the current code because `reconnect_broadcaster` and `reconnect_conn_id` are assigned in the `acp_session_stream` function scope, not inside `_send_callback`. No endpoint code change was made for that suggestion.
- Strengthened the endpoint reconnect regression so it preloads the session event bus with a buffered completion event and asserts the reconnect stream receives the replayed payload before verifying cleanup. This prevents the test from passing trivially when the reconnect broadcaster path is not exercised.
- Targeted regression command passed after rebase and review follow-up: `.venv/bin/python -m pytest tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py::test_acp_session_stream_reconnect_cleans_up_replay_broadcaster tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py::test_ws_broadcaster_allows_unique_consumer_ids -q` (2 passed).
- Bandit passed with 0 findings: `.venv/bin/python -m bandit -r tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py -f json -o /tmp/bandit_acp_reconnect_cleanup_latest_dev.json`.
- `git diff --check` passed.
- Broader file command on latest dev still has one unrelated baseline failure: `test_acp_websocket.py::TestACPRunnerClientPermissions::test_determine_permission_tier_batch` expects `fs.write` to be `batch`, but current dev returns `individual`; the focused ACP reconnect and broadcaster regressions pass.
PR opened: https://github.com/rmusser01/tldw_server/pull/2619 (draft against dev). Draft status is intentional until the human-authored Change summary required by project policy is added.
<!-- SECTION:NOTES:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented MCP-002 remediation for ACP reconnect replay cleanup and refreshed it against latest dev. `acp_session_stream` retains the temporary replay broadcaster and connection id, uses a per-connection WSBroadcaster consumer id, removes the replay connection, and stops the broadcaster in the endpoint finalizer. `WSBroadcaster` accepts an optional consumer id while preserving the existing default. The endpoint regression now proves the reconnect broadcaster path by replaying a buffered completion event and asserting the stream receives it before disconnect cleanup.

Verification on latest dev fd5c152b065c408e4e8ee5f08da41589f21cb7f5:
- Merge-base matched origin/dev after rebase.
- Passed: `.venv/bin/python -m pytest tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py::test_acp_session_stream_reconnect_cleans_up_replay_broadcaster tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py::test_ws_broadcaster_allows_unique_consumer_ids -q` (2 passed).
- Passed: `.venv/bin/python -m bandit -r tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py -f json -o /tmp/bandit_acp_reconnect_cleanup_latest_dev.json` with 0 findings.
- Passed: `git diff --check`.
- Broader run note: `test_acp_websocket.py test_ws_broadcaster.py -q` has one unrelated current-dev failure in `TestACPRunnerClientPermissions::test_determine_permission_tier_batch`.
<!-- SECTION:FINAL_SUMMARY:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [x] #1 Focused ACP websocket and broadcaster tests pass.
- [x] #2 Bandit over touched production files reports no new issues.
- [x] #3 git diff --check passes.
- [x] #4 Backlog task contains verification evidence and final summary.
<!-- DOD:END -->
17 changes: 13 additions & 4 deletions tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,8 @@ async def acp_session_stream(
labels={"component": "acp", "endpoint": "acp_session_stream"},
)
send_callback: Any | None = None
reconnect_broadcaster: Any | None = None
reconnect_conn_id: str | None = None

try:
await stream.start()
Expand All @@ -1276,14 +1278,15 @@ async def _send_callback(message: dict[str, Any]) -> None:
if bus is not None and last_sequence > 0:
from tldw_Server_API.app.core.Agent_Client_Protocol.consumers.ws_broadcaster import WSBroadcaster

broadcaster = WSBroadcaster()
await broadcaster.start(bus)
reconnect_conn_id = f"ws-{session_id}-{id(stream)}"
reconnect_broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{reconnect_conn_id}")
Comment on lines 1278 to +1282

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In Python, assigning to variables inside a nested function (like _send_callback) creates local variables within that function's scope unless they are explicitly declared as nonlocal. Because reconnect_broadcaster and reconnect_conn_id are assigned here without a nonlocal declaration, the outer variables in acp_session_stream remain None. As a result, the cleanup logic in the finally block is completely bypassed when a connection disconnects, leaving the broadcaster and subscriptions active.

Add a nonlocal declaration for both variables at the start of the block to ensure they bind to the outer scope.

Suggested change
if bus is not None and last_sequence > 0:
from tldw_Server_API.app.core.Agent_Client_Protocol.consumers.ws_broadcaster import WSBroadcaster
broadcaster = WSBroadcaster()
await broadcaster.start(bus)
reconnect_conn_id = f"ws-{session_id}-{id(stream)}"
reconnect_broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{reconnect_conn_id}")
if bus is not None and last_sequence > 0:
nonlocal reconnect_broadcaster, reconnect_conn_id
from tldw_Server_API.app.core.Agent_Client_Protocol.consumers.ws_broadcaster import WSBroadcaster
reconnect_conn_id = f"ws-{session_id}-{id(stream)}"
reconnect_broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{reconnect_conn_id}")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against the rebased branch. I did not apply this suggestion because the assignments to reconnect_broadcaster and reconnect_conn_id are in the acp_session_stream function scope, not inside _send_callback; adding nonlocal at that location would be invalid Python. The branch now verifies cleanup from the outer-scope variables with the strengthened reconnect replay regression in 7d82013.

await reconnect_broadcaster.start(bus)

async def _ws_send_str(msg: str) -> None:
await stream.send_json(json.loads(msg))

await broadcaster.add_connection(
conn_id=f"ws-{session_id}-{id(stream)}",
await reconnect_broadcaster.add_connection(
conn_id=reconnect_conn_id,
send_callback=_ws_send_str,
from_sequence=last_sequence,
)
Comment on lines 1278 to 1292

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Reconnect workflow added in endpoint 📘 Rule violation ⌂ Architecture

Reconnect replay/broadcaster lifecycle logic is newly implemented directly inside the v1 endpoint
module, rather than being encapsulated in a core feature module. This increases coupling and makes
the endpoint responsible for business/workflow logic beyond request/response orchestration.
Agent Prompt
## Issue description
New reconnect replay workflow logic (broadcaster creation, unique consumer id selection, replay connection management) was added inside the endpoint module.

## Issue Context
Endpoints should primarily validate/route and delegate workflow/business logic to `/app/core/...` modules so the logic is reusable and testable without HTTP/WebSocket endpoint coupling.

## Fix Focus Areas
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1277-1292]
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1338-1343]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand Down Expand Up @@ -1332,6 +1335,12 @@ async def _ws_send_str(msg: str) -> None:
except _ACP_ENDPOINT_NONCRITICAL_EXCEPTIONS:
logger.exception("WebSocket error for ACP session {}", session_id)
finally:
if reconnect_broadcaster is not None:
if reconnect_conn_id is not None:
with contextlib.suppress(_ACP_ENDPOINT_NONCRITICAL_EXCEPTIONS):
reconnect_broadcaster.remove_connection(reconnect_conn_id)
with contextlib.suppress(_ACP_ENDPOINT_NONCRITICAL_EXCEPTIONS):
await reconnect_broadcaster.stop()
Comment on lines +1338 to +1343

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Cleanup suppresses endpoint exceptions 📘 Rule violation ☼ Reliability

The new cleanup logic uses contextlib.suppress(...) around broadcaster teardown, which can
silently swallow exceptions without logging or justification. This can hide real cleanup failures
and complicate operational debugging.
Agent Prompt
## Issue description
Cleanup logic in `acp_session_stream` suppresses exceptions via `contextlib.suppress(...)`, which violates the rule against silently swallowing exceptions.

## Issue Context
If teardown fails (e.g., unsubscribe/stop issues), the exception is currently dropped without any log line or explicit justification.

## Fix Focus Areas
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1338-1343]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1338 to +1343

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Cleanup breaks on cancellation 🐞 Bug ☼ Reliability

In acp_session_stream(), cleanup awaits reconnect_broadcaster.stop() without shielding against task
cancellation, and asyncio.CancelledError is not part of _ACP_ENDPOINT_NONCRITICAL_EXCEPTIONS. If the
handler is cancelled mid-finalizer, the broadcaster may remain subscribed and the code after that
await (unregister_websocket, stream.stop, quota release) may not run, undermining the reconnect
cleanup goal.
Agent Prompt
### Issue description
`acp_session_stream()` now performs reconnect replay broadcaster cleanup in `finally`, but the awaited cleanup call (`await reconnect_broadcaster.stop()`) is not protected from cancellation. Since `_ACP_ENDPOINT_NONCRITICAL_EXCEPTIONS` does not include `asyncio.CancelledError`, a cancellation raised during cleanup can abort the finalizer early and skip:
- stopping/unsubscribing the replay broadcaster (subscriber leak)
- `unregister_websocket`
- `stream.stop()`
- quota release

This can reintroduce the lifecycle leak under cancellation scenarios (e.g., app shutdown / task cancellation).

### Issue Context
The SSE endpoint explicitly catches `asyncio.CancelledError` and still stops its consumer in `finally`, which is the more robust lifecycle pattern.

### Fix Focus Areas
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1338-1352]
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[125-145]
- tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[3363-3372]

### Suggested fix approach
1. Ensure finalizer cleanup cannot be interrupted mid-way by task cancellation:
   - Wrap awaited cleanup calls in `asyncio.shield(...)`, and/or
   - Catch `asyncio.CancelledError` around cleanup awaits, continue the remaining cleanup steps, then re-raise cancellation after critical cleanup completes.
2. Apply this to at least:
   - `reconnect_broadcaster.stop()`
   - `client.unregister_websocket(...)`
   - `stream.stop()`
3. Keep cancellation propagation semantics correct (cleanup first, then re-raise) similar to how the SSE endpoint ensures `consumer.stop()` runs even when cancelled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# Unregister WebSocket
if send_callback is not None:
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class WSBroadcaster(EventConsumer):

consumer_id: str = "ws_broadcaster"

def __init__(self) -> None:
def __init__(self, consumer_id: str | None = None) -> None:
self.consumer_id = consumer_id or self.consumer_id
self._connections: dict[str, _ConnectionInfo] = {}
self._bus: SessionEventBus | None = None
self._queue: asyncio.Queue[AgentEvent] | None = None
Comment on lines +59 to 63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. wsbroadcaster.init missing docstring 📘 Rule violation ✧ Quality

The modified WSBroadcaster.__init__ method has no docstring, violating the requirement that all
functions/methods include docstrings. Missing docstrings reduce maintainability and make the new
consumer_id behavior harder to understand and audit.
Agent Prompt
## Issue description
`WSBroadcaster.__init__` was modified but still lacks a docstring. The compliance checklist requires docstrings for all functions/methods.

## Issue Context
A new optional `consumer_id` parameter was added and should be documented.

## Fix Focus Areas
- tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[59-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand Down
86 changes: 86 additions & 0 deletions tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
SessionWebSocketRegistry,
)
from tldw_Server_API.app.core.Agent_Client_Protocol.event_bus import SessionEventBus
from tldw_Server_API.app.core.Agent_Client_Protocol.events import AgentEvent, AgentEventKind
from tldw_Server_API.app.core.Agent_Client_Protocol.stdio_client import ACPMessage
from tldw_Server_API.app.services.acp_runtime_policy_service import (
ACPRuntimePolicySnapshot,
Expand Down Expand Up @@ -290,6 +291,91 @@ async def _fake_get_runner_client():
pytest.fail(f"Expected exactly one stream.stop() call, got {lifecycle['stop_calls']}")


@pytest.mark.asyncio
async def test_acp_session_stream_reconnect_cleans_up_replay_broadcaster(monkeypatch):
"""Reconnect replay broadcasters must not leave event-bus subscribers behind."""
Comment on lines +295 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test currently passes trivially because _send_callback is never invoked during the test execution. Since the callback is never called, the temporary reconnect broadcaster is never initialized, meaning bus._subscribers is empty from the start and the cleanup logic is not actually exercised.

Consider publishing an event to the bus or directly invoking the registered callback during the test to ensure the broadcaster is created and active, thereby verifying that the cleanup logic successfully stops and removes it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7d82013. The reconnect cleanup test now preloads the session event bus with a buffered completion event, reconnects with last_sequence=1, asserts the stream receives the replayed payload, and then verifies the temporary broadcaster subscription and runner websocket callback are cleaned up.

Comment on lines +294 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Async test missing required marker 📘 Rule violation ▣ Testability

The newly added async tests use @pytest.mark.asyncio, but this marker is not in the policy’s
approved set. This violates the rule that every test must have exactly one accepted marker.
Agent Prompt
## Issue description
New async tests are marked with `@pytest.mark.asyncio`, but the compliance rule requires exactly one approved marker (`unit`, `integration`, `external_api`, or `local_llm_service`) and `asyncio` is not allowed.

## Issue Context
The repository already configures `asyncio_mode = "auto"`, so async tests do not require the `asyncio` marker to run.

## Fix Focus Areas
- tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py[294-296]
- tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py[59-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

import tldw_Server_API.app.api.v1.endpoints.agent_client_protocol as acp_endpoints

session_id = "session-reconnect-cleanup"
bus = SessionEventBus(session_id=session_id)
await bus.publish(
AgentEvent(
session_id=session_id,
kind=AgentEventKind.COMPLETION,
payload={"detail": "replay-me"},
)
)
stub_client = MockRunnerClient()
release_calls: list[str] = []
streams: list[Any] = []

class _DisconnectingStream:
def __init__(self, *args, **kwargs) -> None:
self.sent_payloads: list[dict[str, Any]] = []
self.stop_calls = 0
streams.append(self)

async def start(self) -> None:
return None

async def stop(self) -> None:
self.stop_calls += 1

async def send_json(self, payload: dict[str, Any]) -> None:
self.sent_payloads.append(payload)

async def receive_json(self) -> dict[str, Any]:
raise WebSocketDisconnect(code=1000)

class _FakeWebSocket:
async def close(self, code: int = 1000) -> None:
return None

async def _fake_authenticate_ws(websocket, token=None, api_key=None, required_scope="read"):
return 1

async def _fake_get_runner_client():
return stub_client

async def _fake_resolve_persona_id(client, *, session_id: str, user_id: int):
return None

def _fake_acquire_quota(*, user_id: int, session_id: str, persona_id: str | None):
return "quota-token", None

def _fake_release_quota(token: str) -> None:
release_calls.append(token)
Comment on lines +295 to +347

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. New test functions lack hints 📘 Rule violation ✧ Quality

The newly added async tests and related helpers introduce multiple function and method definitions
without complete parameter and/or return type annotations (including the test function itself and
nested helpers). This violates the requirement that all function parameters and return values be
explicitly type hinted.
Agent Prompt
## Issue description
Newly added test code (including async tests and helper functions/methods) defines functions without complete type annotations for all parameters and explicit return types.

## Issue Context
PR Compliance ID 224215 requires explicit type hints for all function parameters and return values, and this applies to tests and any helper functions defined within them as well.

## Fix Focus Areas
- tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py[295-347]
- tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py[60-64]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


monkeypatch.setattr(acp_endpoints, "_authenticate_ws", _fake_authenticate_ws)
monkeypatch.setattr(acp_endpoints, "get_runner_client", _fake_get_runner_client)
monkeypatch.setattr(acp_endpoints, "get_session_event_bus", lambda _: bus)
monkeypatch.setattr(acp_endpoints, "_resolve_acp_session_persona_id", _fake_resolve_persona_id)
monkeypatch.setattr(acp_endpoints, "_acp_ws_try_acquire_quota", _fake_acquire_quota)
monkeypatch.setattr(acp_endpoints, "_acp_ws_release_quota", _fake_release_quota)
monkeypatch.setattr(acp_endpoints, "WebSocketStream", _DisconnectingStream)

await acp_endpoints.acp_session_stream(
_FakeWebSocket(),
session_id,
token="valid-token",
last_sequence=1,
)

assert bus._subscribers == {}
assert not stub_client.has_websocket_connections(session_id)
assert release_calls == ["quota-token"]
assert streams and streams[0].stop_calls == 1
replayed_payloads = [
payload
for payload in streams[0].sent_payloads
if payload.get("kind") == AgentEventKind.COMPLETION.value
]
assert len(replayed_payloads) == 1
assert replayed_payloads[0]["session_id"] == session_id
assert replayed_payloads[0]["sequence"] == 1
assert replayed_payloads[0]["payload"] == {"detail": "replay-me"}


class TestACPWebSocketConnection:
"""Tests for WebSocket connection handling."""

Expand Down
21 changes: 21 additions & 0 deletions tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ async def fake_send(msg: str) -> None:
assert received[2]["kind"] == "completion"


@pytest.mark.asyncio
async def test_ws_broadcaster_allows_unique_consumer_ids():
"""Distinct broadcasters should not overwrite each other's bus subscription."""
bus = SessionEventBus(session_id="sess-unique-consumers")
first = WSBroadcaster(consumer_id="ws_broadcaster:conn-1")
second = WSBroadcaster(consumer_id="ws_broadcaster:conn-2")

try:
await first.start(bus)
await second.start(bus)

assert set(bus._subscribers) == {
"ws_broadcaster:conn-1",
"ws_broadcaster:conn-2",
}
finally:
await first.stop()
await second.stop()
assert bus._subscribers == {}


@pytest.mark.asyncio
async def test_ws_broadcaster_summary_filters_thinking():
"""Summary verbosity should drop thinking/tool_call/etc., keep completion."""
Expand Down
Loading