fix(memory): datetime-safe to_prompt_json shim — fixes second-ingest 502 (#169)#172
Conversation
graphiti-core 0.29.0's to_prompt_json calls json.dumps with no default= handler, so entity-summary regen on the second ingest of any entity carrying a LAW datetime attribute (Person.date_of_birth, Matter.engagement_letter_date, Signed.signed_at, ...) raises TypeError: Object of type datetime is not JSON serializable -> 502. Option B (short-term shim): install_datetime_safe_to_prompt_json() rebinds every loaded graphiti_core to_prompt_json reference (a sys.modules sweep, since `from ... import` copies the reference into ~7 modules) onto a default=str wrapper. Identity-idempotent and re-runnable. Wired into _build_graphiti() before any add_episode. Also docs: cloudbuild-deploy.md now describes tag-triggered (server-v* / memory-v*) deploys instead of push-to-main. TDD: graphiti_patches.journey.md + test_graphiti_patches.py (8 tests: bug characterization, full import-site sweep, summarize_nodes crash site, unicode/indent preservation, idempotency) + J5 wiring test. 100% coverage on the new module; memory suite 225 passed, 92.9% total.
Pre-PR architect review (2 MEDIUM, both applied): - _build_graphiti now WARNs when the to_prompt_json sweep rebinds 0 references. Production builds Graphiti once, so a 0 means graphiti-core moved the bind site on an in-range bump — the #169 502 would otherwise return silently. Converts a silent prod regression into an alertable log. - pyproject.toml graphiti-core pin documents that bumping requires re-running test_install_rebinds_summarize_nodes_crash_site as the go/no-go that the shim still covers the crash path. Security review: APPROVE, no findings.
Deploying mayia-staging with
|
| Latest commit: |
e8a6860
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://103ecb91.mayia-staging.pages.dev |
| Branch Preview URL: | https://fix-issue-169-to-prompt-json.mayia-staging.pages.dev |
Code ReviewOverviewWell-scoped fix for the confirmed second-ingest 502 (issue #169). The Findings1. Test mock triggers the warning log path (minor) In 2. Generator fixture annotated as Pytest accepts generator fixtures fine but the return annotation should be 3. Journey.md says "guard flag" but implementation uses identity comparison The journey.md describes idempotency as "a guard flag means a second call is a no-op" — but there is no explicit flag. Idempotency is achieved by 4.
What's Good
VerdictApprove with the |
…-0 WARN test (#169) claude-review round 1: - test_build_graphiti_installs_datetime_patch mocked install to return 0, incidentally firing the rebound-0 WARN branch. Use return_value=7 (happy path) and add test_build_graphiti_warns_when_sweep_rebinds_nothing to cover the WARN branch deterministically via caplog (no longer order- dependent on real-build tests). - journey.md: corrected stale "guard flag" wording — idempotency is identity comparison against the stock function, not a flag. Declined: generator-fixture `-> None` annotation — matches the file's existing fixture convention (_reset_graphiti_singleton, _set_default_env) and Ruff is clean; changing only the new fixture breaks consistency.
Code review — PR #172 (datetime-safe
|
…as shim retirement gate (#169) Filed the upstream root-cause fix (option C): getzep/graphiti#1527 adds a default= handler to to_prompt_json (issue getzep/graphiti#1526). Wire the concrete reference into the shim docstring + journey so the retirement gate is unambiguous when the fix lands in a release.
Code Review — PR #172Overall: LGTM with minor notes. The monkey-patch approach is the right call — sweeping Medium —
|
…line identity check (#169) claude-review round 2: - _json_default now renders date/datetime via .isoformat() (RFC-3339) instead of default=str (space separator). Matches the upstream fix (getzep/graphiti#1527) + the rest of apps/memory (search.py/episodes.py), so the shim emits the same datetime form graphiti-core will once the upstream lands. Decimal/UUID/etc. still fall back to str(). - WARN test uses record.getMessage() (not .message — only set by Formatter.format(); raw LogRecords may lack it). - Inlined _is_original into the sweep to mirror _reset_for_testing's inverse identity check (symmetry). - Added test_patched_falls_back_to_str_for_other_unserializable (Decimal + UUID) — covers the str() fallback branch, back to 100%. Declined again: generator-fixture `-> None` annotation (round-1 #2) — matches the file convention, no type-checker enforces it, Ruff green.
|
Code review round 4 (HEAD: 26c9d09) Overall: Solid fix. sys.modules sweep is the right approach, all prior round findings addressed (isoformat, getMessage, return_value=7, _is_original symmetry). Coverage 92.9% >= 90% gate. One new finding: BUG: unused monkeypatch fixture in test_build_graphiti_installs_datetime_patch (apps/memory/tests/test_graphiti_setup.py) monkeypatch: pytest.MonkeyPatch is declared but never used — all patching is via patch.object() context managers. Remove it: async def test_build_graphiti_installs_datetime_patch( Dead dependency — pytest instantiates it for nothing and misleads readers. PROCESS: staging smoke test still unchecked in the test plan. Confirm before merge — it is the only path that exercises _extract_entity_summaries_batch -> to_prompt_json against production graphiti-core. |
claude-review round 3: test_build_graphiti_installs_datetime_patch declared monkeypatch but patches exclusively via patch.object context managers — dead dependency. Removed.
|
Round 3 response (HEAD a29da25)
Unit layer covers the failing function directly ( |
Code Review — PR #172Overall: The shim design is sound — Finding 1 — MEDIUM: False-positive WARNING on every retry after a failed cold-startFile:
The 502 risk is absent (patch succeeded on call 1), but the WARNING fires on every request during a misconfigured cold-start, burying the real RuntimeError in noise and potentially triggering a false alert. Fix: differentiate "already installed (idempotent)" from "nothing to patch at all" — e.g. return rebound = install_datetime_safe_to_prompt_json()
if rebound:
_LOG.info("memory: installed datetime-safe to_prompt_json (%d references)", rebound)
elif _patch_is_already_active(): # e.g. check any module's attribute
_LOG.debug("memory: datetime-safe to_prompt_json already installed (idempotent)")
else:
_LOG.warning(...) # only genuine "nothing found" caseFinding 2 — LOW: J5 wiring test asserts call-count but not orderingFile:
A minimal improvement: use a Minor (no action required)
|
… assert install ordering (#169) claude-review round 4: - MEDIUM: install runs before env validation, so a failed cold-start (missing env, or build_indices_and_constraints raising) left the patch installed but the singleton uncached; the retry's sweep rebound 0 and fired the "bind site moved / 502 risk" WARN falsely, burying the real error. Added is_datetime_safe_to_prompt_json_installed() predicate and a 3-way branch: rebound>0 -> INFO; rebound==0 & active -> DEBUG (benign idempotent re-run); rebound==0 & not active -> WARN (genuine regression). - LOW: J5 wiring test now asserts install ran BEFORE Graphiti() via a Graphiti side-effect, not just call_count. - Added unit test for the predicate + an idempotent-reinstall-is-quiet test. 229 passed, 100% on graphiti_patches.py, 92.98% total.
Code Review — PR #172Verdict: Approve with minor fixes. The shim is well-designed and correctly addresses the root cause. The Findings
No issues found in
|
Summary
Fixes #169 —
POST /memory/episodes502s on the second ingest touching any already-extracted entity that carries a LAW datetime attribute. graphiti-core 0.29.0'sto_prompt_jsoncallsjson.dumps()with nodefault=handler; entity-summary regeneration (extract_attributes_from_nodes → _extract_entity_summaries_batch → to_prompt_json) serializes the entity'sattributesdict and raisesTypeError: Object of type datetime is not JSON serializable. Our LAW ontology has 16datetime | Nonefields (Person.date_of_birth,Matter.engagement_letter_date,Signed.signed_at, …). First ingest = single write, no summary regen → JSON dump never runs → 200. Second ingest → regen runs → 502. Confirmed live onmemory-v0.0.22(staging), 2026-06-02.Ships option B from the issue (short-term shim). Option C (upstream PR) remains tracked; this shim is retired when it lands + we bump the pin.
What lands (file-by-file)
apps/memory/src/mayia_memory/graphiti_patches.py(new) —install_datetime_safe_to_prompt_json()rebinds every loadedgraphiti_coreto_prompt_jsonreference onto ajson.dumps(..., default=str)wrapper. Asys.modulessweep is required becausefrom .prompt_helpers import to_prompt_jsoncopies the reference into ~7 consumer modules (incl. thesummarize_nodescrash site), so patching only the canonical module misses them. Identity-idempotent (value is _STOCK_TO_PROMPT_JSON) and re-runnable.graphiti_patches.journey.md(new) — surface, locked root cause, sweep rationale, J1–J5.graphiti_setup.py— installs the shim in_build_graphiti()(after telemetry opt-out, before anyadd_episode); WARNs if the sweep rebinds 0 references (a moved bind site on a future bump would otherwise silently reintroduce the 502).pyproject.toml— graphiti-core pin now documents the shim upgrade gate.docs/infra/cloudbuild-deploy.md— (unrelated, pre-existing) tag-triggered (server-v*/memory-v*) deploy docs, bundled at the author's request.Tests
test_graphiti_patches.py(8): bug characterization (stock fn raises), canonical + full import-site sweep,summarize_nodescrash site,default=stroutput, unicode/indent preservation, idempotency.test_graphiti_setup.py: J5 wiring test (_build_graphitiinstalls the shim once).graphiti_patches.py100%; memory suite 225 passed, 15 skipped, 92.9% total (gate 90%). Ruff clean.Test plan
uv run pytest— 225 pass, 92.9% ≥ 90%uv run ruff check && ruff format --checkmemory-v0.0.22Depends-on / Blocks
None. Self-contained.
Out of scope
str | None) — rejected; loses Pydantic datetime parsing across 16 fields.