Context
From PR #192 architecture review. runner.py has 10 pairs of near-identical sync/async methods (e.g. _resolve_permission / _resolve_permission_async, _run_pre_tool_use_sync / _run_pre_tool_use_async). The sync path uses _run_awaitable_sync which spawns a daemon thread with a nested event loop — a known footgun in production async contexts.
Proposal
Wrap sync tool handlers at registration time, then delete all sync twins:
# registry.py — at register time
def register(self, entry: ToolEntry):
if not asyncio.iscoroutinefunction(entry.handler):
original = entry.handler
async def async_wrapper(**kwargs):
return await asyncio.to_thread(original, **kwargs)
entry = replace(entry, handler=async_wrapper)
self._tools[entry.name] = entry
Then remove all 10 _xxx_sync methods in runner.py.
Impact
- ~400 lines removed
- Eliminates daemon-thread + nested-event-loop bridge
- Single code path = easier to reason about and test
Ref
Context
From PR #192 architecture review.
runner.pyhas 10 pairs of near-identical sync/async methods (e.g._resolve_permission/_resolve_permission_async,_run_pre_tool_use_sync/_run_pre_tool_use_async). The sync path uses_run_awaitable_syncwhich spawns a daemon thread with a nested event loop — a known footgun in production async contexts.Proposal
Wrap sync tool handlers at registration time, then delete all sync twins:
Then remove all 10
_xxx_syncmethods inrunner.py.Impact
Ref