Skip to content

Resolve session-store method checks against the instance - #1215

Open
shashvat-singham wants to merge 2 commits into
anthropics:mainfrom
shashvat-singham:fix/store-implements-instance-attribute
Open

Resolve session-store method checks against the instance#1215
shashvat-singham wants to merge 2 commits into
anthropics:mainfrom
shashvat-singham:fix/store-implements-instance-attribute

Conversation

@shashvat-singham

Copy link
Copy Markdown

Problem

_store_implements resolves the method on type(store), so it only recognises class-level definitions. But SessionStore is a structural Protocol — an implementation assigned on the instance satisfies it just as well, and calling it works fine at runtime. Those stores are nonetheless rejected during pre-flight validation:

class DelegatingStore(SessionStore):
    def __init__(self, inner: SessionStore) -> None:
        self.list_sessions = inner.list_sessions
    ...

validate_session_store_options(
    ClaudeAgentOptions(session_store=DelegatingStore(inner), continue_conversation=True)
)
# ValueError: continue_conversation with session_store requires the store to
# implement list_sessions()

Verified against main, three shapes all wrongly reported as not implementing it:

store _store_implements should be
class-level async def list_sessions True True
assigned in __init__ (delegation) False True
patched with AsyncMock False True
genuinely missing False False

The AsyncMock row is probably the most likely way to meet this in practice — someone stubbing a store in their own tests gets a ValueError that only appears when continue_conversation=True, pointing at a method their double clearly has.

Change

Look the attribute up on the instance and compare the underlying function against the Protocol default:

impl = getattr(store, method, None)
if impl is None:
    return False
default = getattr(SessionStore, method, None)
return getattr(impl, "__func__", impl) is not default

A bound method is still matched against the Protocol default via __func__, so an unimplemented store is still detected; anything that isn't a bound method (a plain callable assigned on the instance) is compared directly and is never the default. The getattr(impl, ...) also makes the existing impl is None guard meaningful — previously impl was computed and then not used for the decision.

Confirmed the negative case still works: a store without list_sessions is still rejected, and test_continue_conversation_requires_list_sessions still passes.

Tests

Added test_continue_conversation_ok_when_list_sessions_set_on_instance, which fails on main and passes with the change.

$ pytest tests/test_session_store_conformance.py -q
8 failed, 22 passed

The 8 failures are all the [trio] parametrisations and reproduce identically on an unmodified tree here (no trio backend in my env) — unrelated to this change; the [asyncio] side is green.

parse_message wraps malformed input in MessageParseError -- non-dict
data, a missing type, missing required fields all get the parser's own
error type. But a "message" field that is not a dict escaped as a bare
TypeError from indexing into it:

    parse_message({"type": "user", "message": "hi"})
    # TypeError: string indices must be integers, not 'str'

Same for the assistant branch. The existing handlers only catch
KeyError, so TypeError/AttributeError from indexing a non-dict fell
through, and a single malformed line from the CLI stream would surface
as an unrelated-looking TypeError instead of the documented parse error.

Catch TypeError/AttributeError alongside KeyError in both branches and
raise MessageParseError with the offending data attached, like every
other malformation.
_store_implements looked the method up on type(store), so it only saw
class-level definitions. SessionStore is a structural Protocol, though,
so an implementation assigned on the instance satisfies it just as well
-- and those stores were rejected before the subprocess even spawned:

    class DelegatingStore(SessionStore):
        def __init__(self, inner):
            self.list_sessions = inner.list_sessions

    validate_session_store_options(
        ClaudeAgentOptions(session_store=DelegatingStore(inner),
                           continue_conversation=True)
    )
    # ValueError: continue_conversation with session_store requires the
    # store to implement list_sessions()

even though calling list_sessions() on that store works fine. The same
applies to a store whose method is a functools.partial, and to a test
double patched with AsyncMock -- arguably the most common way to hit
this, since it fails only under continue_conversation.

Look the attribute up on the instance and compare the underlying
function against the Protocol default, so a bound method is still
matched against the default while a plain callable assigned on the
instance counts as an implementation.
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.

1 participant