Skip to content

style: auto-fix ruff lint and formatting (pre-commit preparation) - #3

Closed
Million-mo wants to merge 8 commits into
develop/agenticfrom
fix/code-quality-precommit
Closed

style: auto-fix ruff lint and formatting (pre-commit preparation)#3
Million-mo wants to merge 8 commits into
develop/agenticfrom
fix/code-quality-precommit

Conversation

@Million-mo

Copy link
Copy Markdown
Owner

What

Applies ruff --fix --unsafe-fixes (excluding TC00x rules that break circular imports) and ruff format to clean up 32 auto-fixable lint errors across 27 files.

Why

The repo has a .pre-commit-config.yaml with ruff/mypy/pytest hooks, but pre-commit was never installed. This PR cleans up the backlog of auto-fixable issues so that installing pre-commit hooks won't produce a flood of auto-fixes on every commit.

What Changed

Rule Count Description
SIM105 8 try/except/passcontextlib.suppress()
SIM102 4 Nested if → combined if
SIM108 2 if/else → ternary
PERF401 4 Manual list append → comprehension
F841 3 Unused variable assignment removed
RET504 3 Unnecessary assignment before return
B007 1 Unused loop variable renamed to _
W293 2 Trailing whitespace stripped
D301 2 Raw string prefix for backslash sequences
UP046/UP047 2 Generic[T] → PEP 695 [T] syntax
PLW0108 1 Unnecessary lambda removed
PLR1714 1 != x, != ynot in (x, y)
PYI034 1 __aenter__ returns Self

No logic changes. All modifications are style/formatting only.

What's NOT Changed

  • TC001/TC002/TC003 (move imports to TYPE_CHECKING) — excluded because ruff's auto-fix moves MCPClient to a top-level import, triggering a circular import chain: prompts → mcp_server → agents.context → native_agent.turn → tasks.registry → agentpool_config.task → prompts.
  • BLE001, PLR0915, E501, TRY301, G004, etc. — 206 pre-existing errors on develop/agentic that require manual fixes. Left for follow-up PRs.

Verification

ruff check src/   → 206 errors (was 238, fixed 32)
ruff format src/  → 4 files reformatted
python -c 'import agentpool' → OK
pytest -m unit    → 909 passed, 0 failed

Follow-up

After this PR merges, install pre-commit hooks:

uv tool install pre-commit
pre-commit install
pre-commit install --hook-type commit-msg

This locks down future commits — ruff format + lint + mypy + pytest run automatically before each commit.

Million-mo and others added 8 commits July 1, 2026 16:51
fix(acp): emit TurnCompleteUpdate on RunErrorEvent to unblock frontend
… on turn creation

- acp_mcp_manager: downgrade resources/* and prompts/* client errors to
  debug log level, since these are optional MCP capabilities that clients
  may not support (returns "Internal error")
- agent.py create_turn(): call inject_cancelled_tool_results() before
  creating NativeTurn, fixing UserError when a cancelled turn leaves
  unprocessed tool calls in message history. This aligns create_turn()
  with the existing _stream_events() path which already sanitized.
MCPConnectionPool stored pool-level MCP server configs but never created
connections — get_connection() was only callable lazily and no code path
invoked it for pool-level servers. This left the aggregating provider
empty, causing agents with their own mcp_servers (e.g. engineer) to lose
access to global MCP tools like search_kb.

- Add MCPConnectionPool.initialize() to eagerly connect all pre-registered
  servers during startup
- Call initialize() in SessionPool.start() after cleanup task begins
- Add pool.mcp fallback in _create_native_agent and _reconstruct_acp_agent
  to match SessionController's existing fallback pattern
…anager

When a subagent shares the parent agent's MCPManager (agent._mcp_shared),
pool-level MCP servers are already accessible via the shared manager.
Adding the pool MCP provider again registered the same tools twice — once
as MCP capabilities via self.mcp.as_capability() and once as direct tools
via the aggregating provider — causing pydantic-ai UserError:

  Tool name conflicts with existing tool:
  'streamable_http_http://10.147.252.33:8722/mcp_search_kb'

Fix: guard the pool-level MCP provider addition with
in the subagent creation path (SessionController).
…design

Unify three conflicting MCP tool pipelines into one, using pydantic-ai's
MCPToolset best practices for connection reuse:

- Remove agent.mcp = parent_agent.mcp override (core.py:1005) so subagents
  correctly use pool.mcp from messagenode.py assignment
- Add MCPToolset caching to MCPManager with async as_capability() and
  exit_stack.enter_async_context() for persistent ref-counted connections
- Split aggregating provider to ACP-only (non-ACP handled by as_capability)
- Remove Pipeline 3 (parent MCP provider inheritance)
- Remove dedup hack in get_agentlet()
- Delete MCPConnectionPool entirely (subsumed by MCPManager + MCPToolset cache)
- Rewrite 4 existing inheritance tests + add 8 new tests
- Connection reduction: 12 → 2 for engineer + 5 librarian subagents

Reviewed by Momus (PASS all 5 dimensions) and Oracle (PASS all 7 dimensions
after 3 rounds of review fixing lifecycle spec and async/sync mismatch).
Migrate MCP integration from deprecated MCPServer* classes to MCPToolset
(FastMCP-based). Key decisions:
- Remove to_pydantic_ai() from config classes entirely
- Add to_transport(force_oauth) shared method on config classes
- MCPManager.as_capability() constructs MCPToolset directly
- Add _toolset_cache for connection reuse (one MCPToolset per client_id)
- Cache cleanup via __aexit__ (MCPToolset has no aclose())
- Elicitation handler uses correct 4-arg FastMCP signature
- include_instructions=True explicitly set (preserves current behavior)
- MCP(url=...) still requires synthetic URL (cannot be None)

Reviewed by Momus (plan critic) and Oracle (architecture) across 3 rounds.
All issues resolved.
* fix: suppress optional MCP method errors and sanitize message history on turn creation

- acp_mcp_manager: downgrade resources/* and prompts/* client errors to
  debug log level, since these are optional MCP capabilities that clients
  may not support (returns "Internal error")
- agent.py create_turn(): call inject_cancelled_tool_results() before
  creating NativeTurn, fixing UserError when a cancelled turn leaves
  unprocessed tool calls in message history. This aligns create_turn()
  with the existing _stream_events() path which already sanitized.

* fix: initialize MCPConnectionPool connections on SessionPool startup

MCPConnectionPool stored pool-level MCP server configs but never created
connections — get_connection() was only callable lazily and no code path
invoked it for pool-level servers. This left the aggregating provider
empty, causing agents with their own mcp_servers (e.g. engineer) to lose
access to global MCP tools like search_kb.

- Add MCPConnectionPool.initialize() to eagerly connect all pre-registered
  servers during startup
- Call initialize() in SessionPool.start() after cleanup task begins
- Add pool.mcp fallback in _create_native_agent and _reconstruct_acp_agent
  to match SessionController's existing fallback pattern

* fix: skip pool-level MCP provider for subagents sharing parent's MCPManager

When a subagent shares the parent agent's MCPManager (agent._mcp_shared),
pool-level MCP servers are already accessible via the shared manager.
Adding the pool MCP provider again registered the same tools twice — once
as MCP capabilities via self.mcp.as_capability() and once as direct tools
via the aggregating provider — causing pydantic-ai UserError:

  Tool name conflicts with existing tool:
  'streamable_http_http://10.147.252.33:8722/mcp_search_kb'

Fix: guard the pool-level MCP provider addition with
in the subagent creation path (SessionController).

* spec: rewrite fix-subagent-mcp-inheritance with unified MCP pipeline design

Unify three conflicting MCP tool pipelines into one, using pydantic-ai's
MCPToolset best practices for connection reuse:

- Remove agent.mcp = parent_agent.mcp override (core.py:1005) so subagents
  correctly use pool.mcp from messagenode.py assignment
- Add MCPToolset caching to MCPManager with async as_capability() and
  exit_stack.enter_async_context() for persistent ref-counted connections
- Split aggregating provider to ACP-only (non-ACP handled by as_capability)
- Remove Pipeline 3 (parent MCP provider inheritance)
- Remove dedup hack in get_agentlet()
- Delete MCPConnectionPool entirely (subsumed by MCPManager + MCPToolset cache)
- Rewrite 4 existing inheritance tests + add 8 new tests
- Connection reduction: 12 → 2 for engineer + 5 librarian subagents

Reviewed by Momus (PASS all 5 dimensions) and Oracle (PASS all 7 dimensions
after 3 rounds of review fixing lifecycle spec and async/sync mismatch).

* spec: add migrate-to-mcptoolset change

Migrate MCP integration from deprecated MCPServer* classes to MCPToolset
(FastMCP-based). Key decisions:
- Remove to_pydantic_ai() from config classes entirely
- Add to_transport(force_oauth) shared method on config classes
- MCPManager.as_capability() constructs MCPToolset directly
- Add _toolset_cache for connection reuse (one MCPToolset per client_id)
- Cache cleanup via __aexit__ (MCPToolset has no aclose())
- Elicitation handler uses correct 4-arg FastMCP signature
- include_instructions=True explicitly set (preserves current behavior)
- MCP(url=...) still requires synthetic URL (cannot be None)

Reviewed by Momus (plan critic) and Oracle (architecture) across 3 rounds.
All issues resolved.

* fix: raise RunAbortedError on ErrorData in QuestionTool

When handle_elicitation returns ErrorData (e.g. XML parse error in
questionnaire schema), QuestionTool._execute now raises RunAbortedError
instead of returning a ToolResult with error content.

This ensures pydantic-ai produces RetryPromptPart (failure) instead of
ToolReturnPart (success), which propagates through EventMapper to set
is_error=True on ToolCallCompleteEvent, causing the ACP event converter
to emit completion_status='failed' instead of 'completed'.

Fixes #87

Co-authored-by: @Million-mo

* fix: use ModelRetry instead of RunAbortedError for ErrorData

Per review feedback from @Million-mo: RunAbortedError is not a
ModelRetry subclass, so pydantic-ai does not convert it to
RetryPromptPart. Instead it triggers graceful turn stop, and
cancel_pending_tools() still emits status='completed'.

ModelRetry is the correct exception — it is the only exception
pydantic-ai's ToolManager._raw_execute() catches and converts to
RetryPromptPart, which then propagates through EventMapper to set
is_error=True and produces ACP status='failed'.

Semantic distinction:
- cancel path: RunAbortedError (user actively cancelled)
- ErrorData path: ModelRetry (tool failed, model should retry)

Codebase precedent: subagent_tools.py, fsspec_toolset/helpers.py,
resource_providers/base.py, codemode/helpers.py (4+ usages).
Applied ruff --fix --unsafe-fixes (excluding TC00x rules that break
circular imports) and ruff format to 27 files.

Fixes 32 lint errors:
- SIM105: try/except/pass → contextlib.suppress()
- SIM102: nested if → combined if
- SIM108: if/else → ternary
- PERF401: manual list append → list comprehension
- F841: unused variable assignment
- RET504: unnecessary assignment before return
- B007: unused loop variable
- W293: trailing whitespace
- D301: raw string prefix for backslashes
- UP046/UP047: Generic[...] → PEP 695 generic syntax
- PLW0108: unnecessary lambda
- PLR1714: != comparisons → not in
- PYI034: __aenter__ return Self

Remaining 206 errors are pre-existing on develop/agentic and require
manual fixes (TC00x circular imports, BLE001, PLR0915, etc.).

No logic changes. All 909 unit tests pass.
@Million-mo Million-mo closed this Jul 2, 2026
@Million-mo

Copy link
Copy Markdown
Owner Author

opencode 你好啊

Million-mo pushed a commit that referenced this pull request Jul 8, 2026
* spec: MCP session lifecycle fix — Phase 1

Add OpenSpec change for fixing stale MCP toolset cache and session-scoped
resource lifecycle bugs (#121). Includes:

- proposal.md: What & why (6 lifecycle fixes, no config changes)
- design.md: 8 design decisions (D1-D8) with Oracle + Momus review
- specs/mcp-session-lifecycle: 7 requirements, 14 scenarios
- specs/session-orchestration: Modified requirements for close path
- specs/unified-session-lifecycle: WebSocket disconnect hook
- tasks.md: 7 task groups, 46 tasks (P1a-P1f + E2E)
- tests/mcp_server/test_stale_mcp_connection.py: 5 reproduction tests

Reviewed by Momus (PASS) and Oracle (PASS) after 2 revision cycles.

Closes #121 (spec phase)

* spec: address Gemini Code Assist review comments

4 accepted fixes from dialectical analysis with Oracle:

1. Task 3.1/3.2/3.3: Change _session_connections to
   dict[str, set[tuple[str, int]]] — store (connection_id,
   session_key) pairs so AcpMcpConnectionManager.cleanup_session()
   can look up SessionStreamPair via session_key

2. Task 5.2 (D6): Two-layer cleanup on resume — call both
   SessionController.close_session() (RunHandle lifecycle) AND
   ACPSession.close() (ACP env/signals/prompts). Neither alone
   is sufficient.

3. Task 6.4 (D7): Same two-layer cleanup for WebSocket disconnect

4. Task 2.8: Use try/finally or fixture teardown for test cleanup

Rejected comments (2):
- hasattr(self.agent, 'mcp'): violates AGENTS.md, mcp always set
- hasattr(agent, 'mcp'): same, agent is not None check exists

Already addressed (2):
- Concurrency re-verify after lock: spec's lock-on-context design
  handles this implicitly
- await on_disconnect: type signature makes it obvious

* feat(mcp): add _SessionContext dataclass and session connection tracking

- Add _SessionContext dataclass to MCPManager with per-session state
  (connection_pool, toolset_cache, snapshot, acp_connection_ids, _cleanup_lock)
- Add _session_contexts dict to MCPManager.__init__
- Add _session_connections reverse index to AcpMcpConnectionManager
- Add register_session_connection() method for tracking session→connection mappings

Implements T1 and T6 of fix-mcp-session-lifecycle plan.

* feat(mcp): add session lifecycle methods and ACP cleanup

- get_or_create_session() and update_session_snapshot() on MCPManager (T2)
- add_acp_transport() on MCPManager for session-scoped ACP tracking (T3)
- register_session() returns tuple[SessionStreamPair, int] (T7, GAP-1)
- has_active_sessions() on AcpMcpConnection (T7)
- cleanup_session() with _cleanup_lock on AcpMcpConnectionManager (T7, GAP-12)
- Updated all callers of register_session() to unpack tuple return

* feat(mcp): cleanup_session on MCPManager and wire register_session_connection

- cleanup_session() with per-session _cleanup_lock on MCPManager (T4)
- _acp_mcp_manager field added for ACP cleanup delegation
- connect_acp_mcp_server() gains session_id parameter (T8, GAP-5)
- Returns tuple[str, int] (connection_id, session_key)
- Call site in session.py passes session_id and calls add_acp_transport
- All test callers updated for new signature

* test(mcp): add session lifecycle and ACP cleanup unit tests (T5+T9)

* refactor(mcp): change as_capability to session_id-based API (T10)

- Change as_capability(snapshot=, session_pool=) to as_capability(session_id=)
- Parameterize _make_capability with toolset_cache dict parameter (GAP-7)
- Split _process_snapshot into _process_global_configs and _process_session_configs
- GAP-11: KeyError fallback for concurrent cleanup_session race
- Backward compat: session_id=None processes self.servers with self._toolset_cache

* refactor(agent): update get_agentlet to use as_capability(session_id) (T12)

- Replace as_capability(snapshot=, session_pool=) with as_capability(session_id=)
- GAP-4: Use run_ctx.session_id from AgentRunContext instead of self._session_id
- Remove if/else branching on _mcp_snapshot — as_capability handles internally
- Keep _mcp_snapshot and _session_connection_pool field declarations for compat

* test(mcp): update caching+provider tests for session_id API (T13)

- Update 6 tests in test_mcpmanager_caching.py for new as_capability(session_id) API
- Update 15 failing tests in test_mcp_provider_lifecycle.py to use session context
- Fix static source assertion in test_no_dedup_hack_in_get_agentlet
- All 48 tests pass

* test(mcp): flip stale connection tests to verify fix (T14)

- Rename test_session_resume_returns_stale_toolset → _returns_fresh_toolset
- Rename test_multiple_acp_servers_all_go_stale → _get_fresh_toolsets
- Rename test_disconnect_all_clears_cache → test_cleanup_session_clears_per_session_cache
- All 5 tests now verify the fix instead of documenting the bug
- All tests pass with new session_id API

* feat(session): wire cleanup_session into ACPSession.close and SessionController (T15)

* feat(agent): wire get_or_create_session in SessionController agent creation (T16)

* test(mcp): integration tests for session close lifecycle (T17+T18+T19)

* fix(acp): resume_session close-then-recreate instead of early-return (T20)

* test(acp): resume_session lifecycle tests - close, reconnect, active run (T21+T22+T23)

* feat(acp): add on_disconnect callback to websocket handler (T24)

- Add on_disconnect: Callable[[AgentSideConnection], Awaitable[None]] | None parameter
- Generate UUID4 connection_id on AgentSideConnection at accept time (GAP-3)
- Call on_disconnect in ConnectionClosed handler before conn.close()
- Backward compatible: on_disconnect defaults to None

* feat(acp): implement close_all_sessions_for_connection (T25)

- Add _connection_sessions reverse index on ACPSessionManager
- Add connection_id parameter to create_session() and resume_session()
- Implement close_all_sessions_for_connection() for WebSocket disconnect cleanup
- Idempotent: pops connection_id, iterates sessions, closes via SessionController + ACPSession.close()

* feat(acp): wire on_disconnect to close_all_sessions_for_connection (T26)

- Add on_disconnect parameter to serve(), _serve_websocket(), _serve_streamable_http()
- Wire on_disconnect callback in ACPServer._start_async() closure
- Add session_manager field to AgentPoolACPAgent for shared session tracking
- Create shared ACPSessionManager in ACPServer for cross-connection session tracking
- Add disconnect detection in _serve_streamable_http via recv_task completion
- Fix test_resume_session_is_idempotent -> test_resume_session_closes_old_and_recreates
  (T20 changed resume_session from idempotent to close-then-recreate)

* test(acp): websocket disconnect closes sessions and preserves others (T27+T28)

- T27: test_websocket_disconnect_closes_all_sessions — 2 sessions same conn, disconnect, both closed
- T27: test_websocket_disconnect_preserves_other_connections — 2 conns, disconnect one, other survives
- T28: test_websocket_disconnect_during_run — active run cancelled with 2s timeout on disconnect

* fix(acp): resolve mypy union-attr errors with cast (T32) + add e2e session lifecycle test (T33)

- T32: Use cast() to type session_manager field as ACPSessionManager (not | None) for mypy
- T33: test_e2e_session_lifecycle — full lifecycle: connect→session→MCP→disconnect→reconnect→resume→verify fresh

* fix: resolve CI ruff format and lint errors

- ruff format: reformat 5 files (manager.py, session_controller.py, session.py, test_session_lifecycle.py, test_stale_mcp_connection.py)
- ruff check: shorten docstring in test_acp_session_resume.py (E501)

* fix(mcp): address review — _connection_sessions cleanup, get_or_create leaks

- Fix #1 (Critical): resume_session() now removes session_id from
  _connection_sessions before closing old session. Prevents stale
  connection disconnect from closing the newly resumed session.
- Fix #2 (High): as_capability() uses _session_contexts.get() instead of
  get_or_create_session(). Prevents memory leak when context was already
  cleaned up. Removes dead try/except KeyError code.
- Fix #3 (Medium): cleanup_session() uses _session_contexts.get() and
  returns early if None. Avoids creating throwaway SessionConnectionPool.
- TDD: 3 tests in test_review_fixes.py verify all fixes.

* fix(mcp): wire _acp_mcp_manager, add identity check, consolidate as_capability

Review round 2 fixes:

Fix #4 (Critical): Wire _acp_mcp_manager in ACPSession.__post_init__
- MCPManager._acp_mcp_manager was initialized to None and never set
- cleanup_session() could never delegate to AcpMcpConnectionManager
- Per-session ACP stream pairs and reverse-index entries leaked
- Fix: wire agent.mcp._acp_mcp_manager = acp_agent._mcp_manager in __post_init__

Fix #5 (Medium): Identity check after acquiring cleanup lock
- Concurrent cleanup_session() callers could do redundant work
- All ops were idempotent but wasteful (clearing empty dicts, etc.)
- Fix: check if self._session_contexts.get(session_id) is not ctx after lock

Fix #6 (Medium): Consolidate duplicated fallback in as_capability()
- Three identical 'for server in self.servers:' loops consolidated to one
- Pure readability refactor, zero behavior change

TDD: 3 new tests (2 RED before fix, 3 GREEN after)
- test_cleanup_session_delegates_to_acp_mcp_manager (unit)
- test_acp_session_wires_acp_mcp_manager (integration)
- test_cleanup_session_identity_check_prevents_redundant_work (unit)

213 tests pass, ruff clean.

* test(mcp): add 20 integration tests for session wiring lifecycle

Categories A-D from Oracle integration test plan:
- A (4): Cross-component wiring — cleanup delegation, __post_init__ wiring,
  full close chain, close_all_sessions_for_connection
- B (7): Lifecycle edge cases — full create/cleanup, close/recreate,
  shared connection isolation, WebSocket disconnect, resume, concurrent cleanup
- C (4): State consistency — registry consistency after cleanup/close/resume,
  stream pair unregistration
- D (5): Error paths — ACP manager raises, session close raises,
  MCP cleanup raises, resume old close raises, pool cleanup raises

These tests would have caught the _acp_mcp_manager wiring bug (round 2
review comment #1) that unit tests missed due to component isolation.

* fix(acp): wire connection_id through create_session/resume_session call sites

- Declare connection_id: str | None on AgentSideConnection (replaces monkey-patch)
- Remove # type: ignore[attr-defined] from transports.py connection_id assignments
- Add _get_connection_id() helper on AgentPoolACPAgent using isinstance check
- Wire connection_id= into all 5 create_session/resume_session call sites:
  new_session, load_session, fork_session, resume_session, handler.py
- Fix misleading GAP-11 comment: dict.get() returns None, never raises KeyError

Without this fix, _connection_sessions dict was never populated, making
close_all_sessions_for_connection() always return immediately — the entire
WebSocket disconnect cleanup feature was dead code.

* test(mcp): add 13 E2E integration tests for full MCP session lifecycle

Covers all 13 gap areas identified by Oracle analysis:
- G1: Full create_session → get_or_create_session_agent → MCPManager chain
- G2: as_capability with non-empty ACP snapshot → real MCPToolset
- G3: initialize_mcp_servers → connect_acp_mcp_server → AcpMcpTransport
- G4: Full tool execution through as_capability → MCPToolset → AcpMcpTransport
- G5: SessionController.close_session with real agent + real MCP resources
- G6: resume_session with real ACPSession (not patched)
- G7: Full on_disconnect → close_all_sessions_for_connection chain
- G8: connection_id propagation: create_session populates _connection_sessions
- G9: as_capability during concurrent cleanup (GAP-11 race)
- G10: ACP transport failure during tool execution + cleanup
- G11: Multiple sessions on same connection with real ACPSessions
- G12: Child session inherits parent's ACP transports
- G13: Pool shutdown cleans all session MCP resources

* fix: resolve CI mypy and unit test failures

- server.py: Remove unused type: ignore, use None guard for connection_id
- test_acp_session_resume.py: Add connection_id to expected resume_session call args

* chore(openspec): archive fix-mcp-session-lifecycle and sync specs

- Mark all 46 tasks as complete in tasks.md
- Sync 3 delta specs to main specs:
  - mcp-session-lifecycle (new)
  - session-orchestration (updated)
  - unified-session-lifecycle (updated)
- Archive to openspec/changes/archive/2026-07-07-fix-mcp-session-lifecycle/

* fix: parent session memory leak + on_disconnect in finally (review r3)

- session_controller.py: Replace get_or_create_session() with
  _session_contexts.get() when reading parent snapshot/pool. Prevents
  phantom _SessionContext creation when parent was already cleaned up.
- transports.py: Move on_disconnect callback from except ConnectionClosed
  to finally block. Ensures callback fires on any exception path.
- 3 TDD tests: leak detection, regression guard, disconnect coverage.

* fix(mcp): wire child session ACP manager, add transport callback, fix toolset __aexit__

Three fixes for child session ACP transport registration gaps:

1. Wire _acp_mcp_manager on child agent from parent (session_controller.py)
   - Child sessions created via get_or_create_session_agent() don't go
     through ACPSession.__post_init__, so _acp_mcp_manager stayed None.
     Now copied from parent after copy_pre_created_transports().

2. Add on_session_registered callback to AcpMcpTransport (acp_mcp_transport.py)
   - Optional callback invoked after register_session() with (connection_id,
     session_key). Enables callers to register ACP connections for cleanup
     tracking via register_session_connection().

3. Fix toolset_cache.clear() to call __aexit__ first (manager.py)
   - cleanup_session() called .clear() without closing MCPToolset instances,
     leaking stream pairs and forwarder tasks. Now mirrors disconnect_all()
     pattern: iterate values, call __aexit__(None, None, None) with
     contextlib.suppress(ValueError), then clear.

TDD: 3 tests in test_child_session_acp_fix.py (all GREEN).
252 MCP+ACP tests pass, 0 regressions, ruff clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants