Followup to ec5baaba: the previous commit dropped the unset/empty
telemetryId fallback. Restoring it preserves behavioral parity with
pre-PR code in case telemetryId is ever externally mutated to a
falsy value (the type permits "" even if normal flow never sets it).
Stop overwriting a stable vector-store UUID with the shared SDK
user_id. Existing OSS users were having their telemetry identity
silently swapped on next init, orphaning every prior PostHog event
tagged with the old id (the same regression class as the bug PR
#5040 set out to fix).
The shared SDK id is now adopted only when the vector store reports
"anonymous" / "anonymous-supabase" (fresh-install signals).
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.