Make InMemorySessionStore.append idempotent by uuid - #1216
Open
shashvat-singham wants to merge 3 commits into
Open
Make InMemorySessionStore.append idempotent by uuid#1216shashvat-singham wants to merge 3 commits into
shashvat-singham wants to merge 3 commits into
Conversation
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.
SessionStore.append documents uuid as an idempotency key: "Most entries carry a stable uuid that adapters should treat as an idempotency key (upsert / ignore-duplicate). Entries without a uuid (e.g. titles, tags, mode markers) should be appended without dedup." InMemorySessionStore extended unconditionally, so a re-appended entry was stored twice. That is reachable rather than theoretical: the mirror batcher retries failed batches (3 attempts), and its own docstring notes a retried batch "may partially overlap a prior partial write" -- the overlapping entries then appear twice in the resumed transcript. Skip entries whose uuid is already present, and keep appending entries without a uuid verbatim as the contract requires.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SessionStore.appenddocumentsuuidas an idempotency key:InMemorySessionStore.appendextends unconditionally, so a re-appended entry is stored twice:This is reachable rather than theoretical.
TranscriptMirrorBatcherretries failed batches (3 attempts), and its own docstring says a retried batch "may partially overlap a prior partial write" — which is precisely the case the idempotency clause exists for. The overlapping entries then show up twice in the resumed transcript.Change
Skip entries whose
uuidis already stored; append entries without auuidverbatim, as the contract requires.On the conformance suite (deliberately not changed)
I first added an idempotency check to
run_session_store_conformance, then backed it out. It fails theMinimalStorefixtures intests/test_session_store_conformance.py, which use a plain.extend()— so the contract's "should" reads as advisory guidance for adapter authors rather than a mandatory conformance requirement, and enforcing it would be a breaking change for existing adapters.That does leave the clause untested, so an adapter can pass conformance either way. If you'd like it enforced, I'm happy to send that as a separate PR (it would need the in-tree
MinimalStorefixtures updated too) — but it seemed like your call, not mine.Tests
test_append_is_idempotent_by_uuidandtest_append_does_not_dedup_entries_without_uuid. The first fails onmain.(The
[trio]parametrisations fail identically before and after on my machine — no trio backend installed — so I've filtered toasynciohere.)