Skip to content

Port session_store path to anyio so it works under trio#990

Open
dsmilkov wants to merge 1 commit into
anthropics:mainfrom
dsmilkov:dsmilkov/session-store-anyio
Open

Port session_store path to anyio so it works under trio#990
dsmilkov wants to merge 1 commit into
anthropics:mainfrom
dsmilkov:dsmilkov/session-store-anyio

Conversation

@dsmilkov
Copy link
Copy Markdown

@dsmilkov dsmilkov commented May 25, 2026

The bug

The SDK's core (_internal/query.py, transport/subprocess_cli.py) is
anyio-based and works on both asyncio and trio — it even ships
_internal/_task_compat.spawn_detached precisely so background tasks work
on either backend. But the session_store code path (TranscriptMirrorBatcher,
parts of session_resume.py, and _derive_infos_via_load in sessions.py)
uses raw asyncio.ensure_future / asyncio.Lock / asyncio.wait_for /
asyncio.sleep / asyncio.Semaphore / asyncio.gather.

Under a trio app, passing session_store= to query() or ClaudeSDKClient
crashes at the first batcher flush with:

TypeError: trio.run received unrecognized yield message <Task pending ...>

(an asyncio.Task yielded to trio's event loop).

The fix

Port those three modules to anyio, reusing the SDK's existing spawn_detached:

  • transcript_mirror_batcher.pyanyio.Lock, anyio.sleep,
    anyio.fail_after; eager-flush fires via spawn_detached(self._drain())
    (handle stored in _flush_task: TaskHandle | None); flush() simply
    awaits _drain() (the lock already serializes with in-flight eager
    drains, so the old Task + compare-and-null dance was redundant);
    close() shields its final flush so the last batch lands even when
    teardown runs under a cancelled scope. _swallow_done_exception is
    removed — spawn_detached handles exception surfacing on both backends.
  • session_resume.py_with_timeoutanyio.fail_after;
    _rmtree_with_retryanyio.sleep + anyio.get_cancelled_exc_class().
    The synchronous happy-path rmtree is preserved so cleanup still runs
    under a cancelled scope.
  • sessions.py_derive_infos_via_load
    anyio.CapacityLimiter + anyio.create_task_group() with per-task
    try/except Exception, preserving the "one adapter failure degrades
    that row" semantics.

Behavior

  • asyncio callers: no intended change — anyio dispatches to native
    asyncio primitives; spawn_detached on asyncio is loop.create_task().
  • trio callers: now works.
  • Two deliberate narrowings (noted in case anyone relied on the old edge):
    • On Py3.10 only, an adapter's append() that raises
      asyncio.TimeoutError from its own internals is now retried
      (previously short-circuited). On 3.11+ asyncio.TimeoutError is TimeoutError so there's no change. Adapter-internal timeouts aren't
      our send_timeout, so retrying them is arguably more correct.
    • _derive_infos_via_load no longer swallows BaseException
      (cancellation / KeyboardInterrupt) from a store.load() into an
      empty-summary row — it propagates.

Tests

  • New tests/test_session_store_anyio.py: 7 tests × ["asyncio", "trio"],
    deliberately limited to backend-divergent paths — the existing per-module
    suites already cover behavior in depth under pytest-asyncio. Covered: the
    spawn_detached eager-flush path (the original crash site), eager-flush /
    flush() interleaving order, send-timeout reporting via fail_after,
    close() flushing under a cancelled scope, _with_timeout timeout
    conversion, _rmtree_with_retry under a cancelled scope, and
    list_sessions_from_store with one failing load.
  • tests/test_transcript_mirror.py updated for the removed
    _swallow_done_exception, the anyio.sleep patch target, and the
    TaskHandle type of _flush_task.
  • Full suite: 805 pass, 3 skip (live-only). mypy --strict clean, ruff clean.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.66667% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@9970096). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/claude_agent_sdk/_internal/session_resume.py 83.33% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #990   +/-   ##
=======================================
  Coverage        ?   89.31%           
=======================================
  Files           ?       23           
  Lines           ?     3988           
  Branches        ?        0           
=======================================
  Hits            ?     3562           
  Misses          ?      426           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ons to anyio so session_store works under trio

The SDK core is anyio-based and works on both asyncio and trio, but the
session_store path (TranscriptMirrorBatcher, session_resume helpers,
_derive_infos_via_load) used raw asyncio primitives, so passing
session_store= to query() / ClaudeSDKClient crashed under trio at the
first batcher flush.

Port those three modules to anyio (Lock/sleep/fail_after/CapacityLimiter/
create_task_group) and the SDK's own spawn_detached. flush() simplifies
to awaiting _drain(); close() shields its final flush so the last batch
lands under a cancelled scope. No behavior change for asyncio callers.

New tests/test_session_store_anyio.py runs 7 tests across both backends, limited to backend-divergent paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dsmilkov dsmilkov force-pushed the dsmilkov/session-store-anyio branch from c037b4a to aa615ea Compare May 25, 2026 15:13
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