Review pass on the PostHog identity-stitching fix:
- Gate _maybe_alias_anon_to_email and _maybeAliasAnonToEmail on telemetry
being enabled. Previous version still read ~/.mem0/config.json and
persisted telemetry.aliased_to even when MEM0_TELEMETRY=false, which
meant re-enabling telemetry later would skip identity merging forever.
Python checks client_telemetry.posthog; TS exports a new
isTelemetryEnabled() getter from telemetry.ts.
- Trim capture_identify properties to {$anon_distinct_id, client_source}.
The full platform metadata duplicated capture_event's payload and is
not needed for $identify; PostHog enriches the person profile from
subsequent events anyway.
- Flatten config.ts: drop the four-helper structure (loadNodeModules /
configPath / loadRawConfig / read+write) into one getNodeFs() + a
shared loadConfig(). Same behaviour, ~30 fewer lines, easier to read.
- Drop the tautological Mem0AnonIds shape test and the
void createMockFetch hack at the end of the TS test file.
- Add tests for the new telemetry-disabled gate (Python + TS).
Logic traced end-to-end: 114 Python tests + 590 TS tests pass. Ruff and
Prettier clean. Zero new typecheck errors.
The Python sync/async MemoryClient and the TS MemoryClient were silently
switching their PostHog distinct_id from an anonymous on-disk UUID to the
user's email post-ping, without sending the merge instruction. PostHog
ended up tracking each user as 2-3 disconnected personas (OSS UUID, CLI
anon UUID, email), and only ~4 $create_alias events fired across ~300K
users. The Python CLI does this correctly today; this change ports the
same pattern to MemoryClient.
- mem0/memory/setup.py: make setup_config() idempotent so it backfills a
top-level user_id for CLI-first users (the CLI writes
telemetry.anonymous_id, not user_id, which left OSS telemetry silently
dropped). Add read_anon_ids() and mark_aliased() helpers.
- mem0/memory/telemetry.py: add AnonymousTelemetry.capture_identify() that
posts a PostHog $identify event with $anon_distinct_id. Add "$identify"
to _LIFECYCLE_EVENTS so it bypasses the 90% sampling drop.
- mem0/client/main.py: at the end of __init__ for both MemoryClient and
AsyncMemoryClient, fire $identify for each unaliased anon ID and
persist telemetry.aliased_to so the alias only happens once per
(anon_id, email) pair, ever. All telemetry calls swallow exceptions.
- mem0-ts/src/client/telemetry.ts: mirror with captureIdentify().
- mem0-ts/src/client/config.ts (new): node-only readMem0AnonIds() and
markMem0Aliased() using async dynamic imports of fs/path/os, gated on
process.versions.node so browsers no-op cleanly.
- mem0-ts/src/client/mem0.ts: call _maybeAliasAnonToEmail() inside
_initializeClient after ping resolves the email.
Tests cover the matrix (OSS-only, CLI-only, both, already-aliased) plus
failure modes (malformed JSON, read-only fs, posthog errors, browser
env). Telemetry never raises.