epic-3/story-2: Support Mutable Visitor State on Contexts - #34
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements mutable visitor state persistence (Story 3.2) by introducing Context.set_attributes() to persistently update and merge visitor attributes via an injected DataStore, and rehydrating these attributes during context creation in Core. The review feedback suggests several performance and API flexibility improvements: typing the attributes parameter as Mapping[str, Any] instead of dict[str, Any] for better flexibility, and avoiding unnecessary dictionary copying/object instantiation in ContextState.with_attributes() and Core._hydrate_visitor_attributes() when no new attributes are provided.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| # --- mutable visitor state (Story 3.2) --------------------------------- | ||
|
|
||
| def set_attributes(self, attributes: dict[str, Any]) -> None: |
There was a problem hiding this comment.
Using Mapping[str, Any] instead of dict[str, Any] for the attributes parameter is more Pythonic and flexible. It allows callers to pass other mapping types (such as MappingProxyType or custom read-only dictionaries) and aligns with the type hints used elsewhere in the SDK (e.g., visitor_attributes is typed as Optional[Mapping[str, Any]]).
| def set_attributes(self, attributes: dict[str, Any]) -> None: | |
| def set_attributes(self, attributes: Mapping[str, Any]) -> None: |
| merged = dict(self.visitor_attributes) | ||
| if new_attributes: | ||
| merged.update(new_attributes) | ||
| return ContextState( | ||
| visitor_id=self.visitor_id, | ||
| snapshot=self.snapshot, | ||
| visitor_attributes=merged, | ||
| ) |
There was a problem hiding this comment.
If new_attributes is empty or None, we can return self directly. Since ContextState is a frozen dataclass, returning self is perfectly safe, avoids unnecessary dictionary copying, and prevents creating a new ContextState instance.
| merged = dict(self.visitor_attributes) | |
| if new_attributes: | |
| merged.update(new_attributes) | |
| return ContextState( | |
| visitor_id=self.visitor_id, | |
| snapshot=self.snapshot, | |
| visitor_attributes=merged, | |
| ) | |
| if not new_attributes: | |
| return self | |
| merged = dict(self.visitor_attributes) | |
| merged.update(new_attributes) | |
| return ContextState( | |
| visitor_id=self.visitor_id, | |
| snapshot=self.snapshot, | |
| visitor_attributes=merged, | |
| ) |
| merged = dict(stored) | ||
| if visitor_attributes: | ||
| merged.update(visitor_attributes) | ||
| return merged |
There was a problem hiding this comment.
If visitor_attributes is empty or None, we can return stored directly. This avoids an unnecessary dictionary copy when there are no caller-supplied attributes to merge with the persisted state.
| merged = dict(stored) | |
| if visitor_attributes: | |
| merged.update(visitor_attributes) | |
| return merged | |
| if not visitor_attributes: | |
| return stored | |
| merged = dict(stored) | |
| merged.update(visitor_attributes) | |
| return merged |
…ibutes — tests (RED) Beads: ai-driven-product-dev-vi7i Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ibutes — implementation (GREEN) Beads: ai-driven-product-dev-vi7i Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RED) Beads: ai-driven-product-dev-ni5e Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_context — implementation (GREEN) Beads: ai-driven-product-dev-ni5e Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n round-trip — tests (verification) Beads: ai-driven-product-dev-xuci Verification task (Task 4): no production code change; proves FR25 determinism, AC#5 overlay/persisted distinction, AC#2/#3 round-trip via qs-06 fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c413d52 to
52f5a08
Compare
c9d9d13 to
2298a37
Compare
|
Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup. |
Story 3.2: Support Mutable Visitor State on Contexts
Part of sprint
sprint/2026-04-06-convert-python-sdk. Stacked on epic-3/story-1 (#33 → #32 → #31 → #30 → #29 → #24).Adds the public
Context.set_attributes(attributes: dict[str, Any]) -> Nonemutation surface on top of the Story 1.3Context/ContextStatefoundation and the Story 3.1DataStorepersistence boundary. Honors audit finding F-008.What was built
domain/context_state.py— immutableContextState.with_attributes(...)returning a merged copy (frozen, L0-clean; no in-place mutation).context.py—Context.set_attributes(...)rebindsContextStateto the merged copy and persists it through the injectedDataStore(_persist_visitor_state()); request-time overlay seam (_resolve_visitor_attributes) left unchanged and distinct (overlay > persisted-state precedence).core.py—create_contextnow hydrates per-visitorContextStatefrom the store (_hydrate_visitor_attributes) and injects the single per-CoreDataStoreinto the context seam, so a freshcreate_context(visitor_id)rehydrates the persisted update (AC#3).ports/storage.py— added stable visitor-scopedvisitor_state_key()(mirrors thededup:namespacing); no second store, no parallel dict, no second key scheme.Context.updateVisitorProperties→_dataManager.putData(visitorId, {segments})(F-008-corrected anchor).Audit finding honored
ContextStatewith a merged copy persisted through theDataStore(corrected from the stalegetVisitorProperties-only deep-merge framing). Baked into the spec; verified in code.Driver-resolved precedence (auto-delegated, sprint mode)
ContextStatehydrate/save increate_context(onlytracking/deduplication.pyused the store) — minimal hydrate/save wiring added through the SAME single per-CoreDataStoreand a newstate:<visitor_id>key. Logged as a "your call" auto-delegation.tests/test_layering.py(import-linter not installed in MVP toolchain);context.pydepends only on theports/storage.pyprotocol.Beads
Epic
ai-driven-product-dev-u4mw; tasks-vi7i,-ni5e,-xuci— all closed.Tests
396 → 415 (+19), zero regressions. Extended flat
tests/test_context_creation.py(unit) +tests/integration/test_in_memory_store.py(qs-06 round-trip).Sprint-mode notes
work/2026-06-08-support-mutable-visitor-state-on-contexts/readiness-assessment.md.🤖 Generated with Claude Code