Commit Graph

2695 Commits

Author SHA1 Message Date
Saket Aryan af7dfc8f64 merge main into pr5 after #7322 was squash-merged
Same squash divergence as pr2 and pr4. Nine conflicts, three of them real and
six generated.

build.py: kept this branch's side, which carries the portable-bundle fix main
does not have. Regenerated all six _harness_id.py from it rather than resolving
them by hand, and verified the outcome: the portable bundle declares no
application and each native one still names its host.

deepseek-plugin/src/index.ts: kept this branch's side. Main has the comment
claiming the backend allowlist already recognizes DEEPSEEK_HARNESS, which is not
true until mem0ai/platform#3602 ships; this branch carries the correction.

test_uninitialised_identity.py: append-only, as on pr4. Our side kept whole.

Bundles clean for all six hosts, 318 passed 8 skipped, deepseek and pi-agent
suites green.
2026-09-18 13:47:56 +05:30
Saket Aryan 4e38b057fd fix(plugins): count installs once, and re-resolve the email when the key changes (#7325) 2026-09-18 13:46:25 +05:30
Saket Aryan 3362999095 fix(plugins): stop delivering telemetry events twice, and stop losing parked ones (#7324) 2026-09-18 13:43:33 +05:30
Saket Aryan 012cd32c3a fix(plugins): report the plugin that produced the event, not the one that sent it (#7323) 2026-09-18 13:42:34 +05:30
Saket Aryan e4e0307ae6 fix(plugins): say what telemetry actually sends, and salt the hashes (#7322) 2026-09-18 13:23:05 +05:30
Kartik 84bf468176 docs: add practical Mem0 Copilot guide (MEM-6344) (#7337) 2026-09-18 06:54:04 +05:30
Saket Aryan 6aa1f60503 fix(client): drop the unused import CI's lint caught
Left behind when the async construction test was removed. ruff check now passes
on mem0/ and tests/.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:55:17 +05:30
Saket Aryan 3d0521cad4 Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-17 19:44:14 +05:30
Saket Aryan d0799f1cb5 Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-17 19:44:14 +05:30
Saket Aryan afc3d02a80 fix(plugins): sweep temp files a killed process left behind
Review finding, and the adjacent case to the one this PR already fixed.
_sweep_debris collects *.partial and *.corrupt; _write_identity and
_install_salt both create telemetry-*.<pid>.tmp and unlink it in a finally,
which a SIGKILL skips. Its own docstring reasoning, that no glob in the module
matches them so nothing else ever will, applies equally.

Collected on the stale window rather than the expiry window: unlike a quarantined
batch a temp file carries nothing worth keeping for diagnosis.

276 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:44:14 +05:30
Saket Aryan df4b88685b fix(client): repair the missed _bounded_stack call site, and test that a client constructs
Reported as a blocker by an independent re-review, and correctly: the previous
commit changed _bounded_stack to take the caller's entries and our own entry
separately, updated _apply_client_headers, and missed _client_stack. That runs on
every construction path, so every MemoryClient(...) raised TypeError. A total SDK
outage, introduced by the fix for a cosmetic truncation bug.

The whole suite stayed green because nothing constructed a client. That is the
actual defect here, so the test file exists as much for the gap as for the bug:
it builds a client, asserts the header reaches it, and covers the two bounding
rules directly. Confirmed it fails against the broken call site and passes
against the repaired one.

AsyncMemoryClient is deliberately not constructed: its validation path makes a
real request to /v1/ping/, and a unit test needing the network is worse than
none. It shares _client_headers with the sync client, which is what the
construction test guards.

tests/test_client_surface_headers.py 5 passed, plugin suites 312 passed 8
skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:40:17 +05:30
mintlify[bot] f135cb9949 SEO & metadata audit: shorten hermes description (#7359)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-09-17 19:32:03 +05:30
Saket Aryan 3a72dfdc52 fix(integrations): reserve our own slot in the client stack, and drop whole entries
Two review findings on this PR.

All three client-stack implementations appended our entry and then trimmed to
four, so whenever a caller already sent four entries the one dropped was exactly
the one the function exists to add. We vanished from our own stack while every
caller claim survived. The character cap was worse: slicing the joined string
severs an identifier, and the platform parses the fragment as a real client, so a
truncated tail arrives as a client literally named "me". Both caps now drop whole
entries and the reserved slot is ours, in the Python SDK, the TypeScript SDK and
pi-agent. mcp-server has the same fix on the platform branch.

The deepseek comment claimed the backend's allowlist recognizes DEEPSEEK_HARNESS.
This PR introduced that wording, replacing a neutral one. It is not true until
mem0ai/platform#3602 ships, so it now states the dependency.

Two pi-agent tests: our entry survives a full caller stack, and every surviving
entry is whole rather than a severed tail. Python side verified directly, a
4-entry caller stack keeps mem0-python and long entries are dropped whole.

312 passed 8 skipped, pi-agent 96, bundles clean, TS SDK builds.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:29:13 +05:30
Saket Aryan 8e59181edf Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-17 19:28:11 +05:30
Saket Aryan a0bbf748c2 Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-17 19:28:11 +05:30
Saket Aryan 4edfaabc06 fix(plugins): collect quarantined batches instead of leaving them on disk forever
Review finding. _sweep_debris globbed only *.partial. The *.corrupt files this
PR writes when a batch cannot be decoded are matched by no glob in the module, so
they accumulated for the life of the install.

Collected on the expiry window rather than the stale window, deliberately: a
quarantined batch is the only remaining evidence of events that could not be
delivered, so someone chasing a report of missing telemetry has to be able to
find a recent one. Debris keeps the short window; it carries nothing.

One test, asserting both halves: a recent quarantine survives and an expired one
does not.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:28:08 +05:30
Saket Aryan 74ca467b28 Merge branch 'pr2/source-at-record-time' into pr3/spool-delivery 2026-09-17 19:27:43 +05:30
Saket Aryan 8abedca24a Merge branch 'pr1/telemetry-privacy-docs' into pr2/source-at-record-time 2026-09-17 19:27:43 +05:30
Saket Aryan c043e97673 fix(plugins): read the salt before minting one, and stop overclaiming backend support
Two findings from an independent review of this branch.

_install_salt went straight to create, fsync, link, unlink on every call. All but
the first process finds the salt already published, so each hook paid an fsync to
discover that, on a path documented as appending a line and returning. Hooks are
separate processes firing on every tool call inside a few-second budget.
Measured: cold process one fsync, warm process zero, same salt.

The deepseek README said the backend recognizes DEEPSEEK_HARNESS so usage
surfaces by name. It does not yet. That value, along with STRANDS, ZAPIER,
MEM0_PLUGIN, PI_AGENT and VERCEL_AI_SDK, buckets into OTHERS until
mem0ai/platform#3602 ships, so the README now states the dependency and links it.
The neighbouring comment in mem0-strands was already accurate and is unchanged:
it says recognized values live in the allowlist without claiming this one is in
it.

265 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-17 19:27:39 +05:30
ANIRUDDHA ADAK f5220ff8d4 fix(security): bump next to 15.5.24, patches GHSA-p293-qw3h-jr36 (#7320) 2026-09-17 19:23:45 +05:30
Saket Aryan 92aa8a1de1 fix(vercel-ai-sdk): inject the provider version at build instead of hardcoding it
Review finding from @karthik-indla on this PR. src/mem0-utils.ts carried
PROVIDER_VERSION = "3.0.2" as a literal. It matches package.json today and
misreports the client version from the next release bump onwards, which is the
one thing X-Mem0-Client exists to carry. Every other client in the repo injects
at build: mem0-ts via __MEM0_SDK_VERSION__, the Python side via
importlib.metadata, the CLIs via __CLI_VERSION__.

tsup now defines __MEM0_PROVIDER_VERSION__ from package.json, and the source
falls back to "dev" only when run unbundled, such as in tests. Verified in the
built bundle: PROVIDER_VERSION resolves to "3.0.2" and no placeholder survives.

resolveJsonModule is enabled alongside it, matching mem0-ts, because tsc
--noEmit covers tsup.config.ts and the package.json import fails without it.

Type check clean, build clean. The jest suite's failure is pre-existing and
unrelated: it requires a live MEM0_API_KEY, confirmed by running it on a stashed
tree. Python side 311 passed, 8 skipped; pi-agent 94 passed.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 22:16:20 +05:30
Saket Aryan 2266eccf07 fix(plugins): make the install marker durable before claim_install returns
Review nit from @karthik-indla on this PR. A hard kill between the O_EXCL open
and the buffered write reaching disk left a marker that exists but parses to
nothing: is_first_run reads it as claimed, so that install is never counted, and
claim_version_change cannot read a version out of it.

Not temp-and-rename, which is what the equivalent fixes in this stack use: the
O_EXCL open is what makes this claim exclusive across concurrently starting
sessions, and a rename would clobber rather than lose the race. The content is
the part that needed making safe, so it is flushed and fsynced before the call
returns.

The recovery path stays as the backstop: _repair_install_state already rewrites
an unparseable marker so version tracking resumes.

304 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 22:15:07 +05:30
Saket Aryan 75a2952004 Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-16 22:15:07 +05:30
Saket Aryan 8a014f299a Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-16 22:14:45 +05:30
Saket Aryan 40287f6f04 fix(plugins): a batch that could not be read is not a delivered batch
Two review findings from @karthik-indla on this PR.

_drain returned (0, True) on any read failure, so a claim nothing was posted
from counted as fully delivered. flush() then carried on to the next claim as
though this one had arrived, and the single signal that says the run went badly
never fired. The two cases are now separated: undecodable content is still
quarantined and reported delivered, because there is nothing left to send and
the rest of the run should continue, while an OSError leaves the file exactly
where it is and reports undelivered. Quarantining there would discard events
over a transient filesystem error, and nothing ever re-globs .corrupt.

Retries had no time backoff. _release_claim backdated straight to
immediately-reclaimable, so two senders meeting one momentary failure could walk
a batch from attempt 0 to the limit within seconds and discard it, when a retry
a minute later would have delivered. Releases now carry a cooldown that grows
with the attempts already spent, clamped so the mtime never lands in the future
and reads as a live lease.

Four tests: an unreadable batch is neither delivered nor quarantined,
undecodable content still is quarantined so one torn file cannot block every
later claim, and attempts cannot be burned without waiting. The expiry test now
ages the file between flushes, which is the wall time a real retry waits.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 22:14:42 +05:30
Saket Aryan dddeb4572b Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-16 21:08:46 +05:30
Saket Aryan 9f8b106f8e Merge branch 'pr1/telemetry-privacy-docs' into pr2/source-at-record-time 2026-09-16 21:08:45 +05:30
Saket Aryan ed7b09884c Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-16 21:08:45 +05:30
Saket Aryan 0377b9a85e Merge branch 'pr2/source-at-record-time' into pr3/spool-delivery 2026-09-16 21:08:45 +05:30
Saket Aryan 3fd4949040 fix(plugins): keep the salt working where hardlinks are not supported
Self-review of the atomic-publish fix. Some network mounts and container volumes
reject os.link, and the outer handler swallowed that into "no salt", which meant
repo_hash and session_hash were dropped on every run for that whole cohort. The
race being closed is narrow; losing the hashes for an entire filesystem is not a
fair trade.

Falls back to claiming the name with O_CREAT|O_EXCL and writing, which is what
this did before. The empty-file window reopens there, but it is benign now: a
reader landing in it gets "" and omits the hash for that process rather than
caching a guessable path digest, which was the actual defect.

266 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 21:08:43 +05:30
Saket Aryan 64a01ab31e fix(pi-agent): attribute the shared client, not two command call sites
Review finding from @kartik-mem0 on this PR.

Only the explicit slash commands set a source. Automatic recall at entry.ts:80,
the capture path, the memory tools and deletion all go through the same client
constructed at entry.ts:31 with no attribution, so everything except the
commands still reached the platform as generic SDK traffic. That is most of the
plugin's traffic.

Identity is now stamped once on the shared client. Deliberately by mutating
client.headers rather than through the SDK's MEM0_SOURCE environment support:
this package pins mem0ai ^3.0.7, the installed build has no such support, and
setting an environment variable it does not read would have looked like a fix
and changed nothing. Every request method in the published client sends
this.headers, so this covers all of them and keeps working when the SDK gains
the env path.

Set-once and append-only are preserved, so a wrapper that already named a
surface keeps it and the client stack accumulates rather than being replaced.
PLATFORM_SOURCE moves into the new module and commands.ts imports it; the body
source stays on those two calls because that is what the backend reads when the
header is absent.

Five tests for the header contract, plus the existing suite: 94 pi-agent tests
pass and the tsup DTS build is clean. Python side 307 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 20:37:57 +05:30
Saket Aryan 9524c238db Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-16 20:36:27 +05:30
Saket Aryan 6d89b3b33e fix(plugins): rotate the anonymous id when the account goes, and verify legacy rows
Three review findings from @kartik-mem0 on this PR.

Anonymous id reuse, reported twice and one defect. The id is offered to PostHog
as $anon_distinct_id on first sign-in and that merge is permanent, so keeping it
after a logout or a key change puts every later anonymous event on the account
that just left. It is now rotated on both routes, and 'aliased' is cleared with
it so the fresh id can be merged into whatever account comes next. Rotation is
deliberately not triggered by a plain lookup failure with no cached email: there
is no previous account to leak to, and churning ids there would fragment the
person for anyone offline on first run.

Legacy rows are verified instead of adopted. A row written before fingerprints
existed carries an email and no fingerprint; adopting the current key bound that
key to the previous account's email permanently, and every run after agreed with
itself. It now resolves once and takes the answer. If the lookup fails it keeps
the cached email and retries next flush rather than dropping a real attribution,
which is safe because the network that failed /v1/ping/ is about to fail the
PostHog POST too. My original comment justifying the shortcut claimed the check
would cost a request on every flush forever; that was wrong, the fingerprint is
stored after one success.

A failed upgrade claim is released. The sentinel was created before the marker
rewrite and left behind if the rewrite failed, so claim_version_change returned
early on every later run and that version's upgrade was never recorded again.

Five tests, covering both rotation routes, legacy verification, the firewalled
legacy case, and retrying a failed upgrade claim.

300 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 20:36:23 +05:30
Saket Aryan 88bd5f164f Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-16 20:35:23 +05:30
Saket Aryan d8c99fb405 fix(plugins): check the lease before judging a claim exhausted
Review finding from @kartik-mem0 on this PR, and the most serious one: it loses
events, which is what this PR exists to prevent.

_claim_parked judged exhaustion before liveness. Claiming a parked file bumps
its attempt count and refreshes its mtime, so the moment a sender takes the
final attempt the file looks exhausted to every other sender while its owner is
actively draining it. The second sender unlinked it, and everything in that
batch was gone.

The liveness check now runs first, so a batch under a live lease is skipped
whatever its attempt count. The cleanup is deferred, not cancelled: once the
lease lapses, the same exhausted file is reaped on a later run.

Two tests. The first walks a batch to the final attempt and asserts a second
sender neither takes it nor deletes it, and that the events are still in it. The
second asserts an abandoned exhausted batch is still discarded once its lease
lapses, which is the over-correction to guard against. Confirmed the first fails
against the previous ordering.

288 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 20:35:13 +05:30
Saket Aryan 2ed501a43c Merge branch 'pr1/telemetry-privacy-docs' into pr2/source-at-record-time 2026-09-16 20:33:57 +05:30
Saket Aryan 26760b00b1 Merge branch 'pr2/source-at-record-time' into pr3/spool-delivery 2026-09-16 20:33:57 +05:30
Saket Aryan 7710a4e180 fix(plugins): publish the salt atomically, and omit the hash when there is none
Review finding from @kartik-mem0 on this PR.

O_CREAT|O_EXCL then write leaves a window where the salt file exists and is
empty. Hooks are short-lived processes firing on every tool call and people run
several agent windows, so a concurrent reader lands in that window, reads
nothing, and falls back to a digest of the salt file's own path, memoized for
its whole run. That path is guessable, so the race silently replaced the privacy
control with something an attacker can compute, and hashed the same repository
two ways depending on timing.

The value is now written to a private temp file, fsynced, and published with
os.link, which is atomic and fails if another process already published one.
Link rather than replace, so losing the race adopts their salt instead of
clobbering it. The temp file is removed either way.

The derived fallback is gone rather than fixed. _scoped_digest returns "" when
there is no salt and record() omits the property, because an unsalted digest
over a git remote or a home-directory path is close to plaintext, and shipping
one under a name that says hash is worse than sending nothing.

Three tests: the racing reader never sees the name half-written, a second writer
adopts the first's salt and leaves no temp file, and an unwritable data
directory drops the property instead of emitting a weak one. The old test
asserted the fallback behaviour and is replaced.

265 passed, 8 skipped.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 20:33:54 +05:30
Paurush Mittal 0df3e4b87d fix(docs): correct rendered titles and remaining SEO links (#7344) 2026-09-16 17:14:33 +05:30
Saket Aryan f80d21fa06 fix(plugins): do not claim a host application the portable bundle cannot know
Review point. The portable bundle is built with host "coding-agent", and the
build wrote that straight into PLATFORM_APPLICATION, so every portable install
sent X-Application: coding-agent.

That value is not in the platform's allowlist, so it was already being dropped
server-side. The effect was the worst of both: the wire claimed we knew the
editor, the stored event recorded that we did not, and nothing said which was
right. An absent header says the same thing honestly and costs a lookup.

HARNESS_ID stays "coding-agent". It is the PostHog-side label, it is true, and
grouping portable installs together there is useful.

Native bundles are unchanged apart from the regenerated comment. Covered by two
new build tests: portable declares no application, and each native names the
host it was generated for.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-16 02:49:11 +05:30
Saket Aryan 1304ffd4a1 Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-15 22:44:20 +05:30
Saket Aryan b66b70c212 Merge branch 'pr1/telemetry-privacy-docs' into pr2/source-at-record-time 2026-09-15 22:44:20 +05:30
Saket Aryan f2f58b5a64 Merge branch 'pr2/source-at-record-time' into pr3/spool-delivery 2026-09-15 22:44:20 +05:30
Saket Aryan 422a9caf8d Merge branch 'pr4/install-marker-and-identity' into pr5/surface-headers 2026-09-15 22:44:20 +05:30
Saket Aryan f8a9d524be docs(openclaw): correct the anonymity claim to match how it identifies events
The sweep in aa770aa6 deliberately left this page alone, reasoning that
hashing the email is materially different from sending it. Reading
integrations/openclaw/telemetry.ts does not support that: distinctId() is an
unsalted sha256 of the account email, and Mem0 holds the emails it is derived
from, so recovering the account is a table join. resolveEmail() also rewrites
already-queued events onto that id, and identifyAnonymous() fires a PostHog
$identify that merges the prior random id into it for good.

That is pseudonymous, not anonymous, and it is the same mismatch between the
stated privacy posture and the wire format that this stack exists to close.
The opt-out is unchanged and still correct.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-15 22:44:17 +05:30
Saket Aryan 2097e29edb docs(plugins): restore the telemetry sweep this branch's merge reverted
The pr4 -> pr5 merge resolved eleven documentation files to the pre-sweep
side, undoing aa770aa6 in its entirety. Because the stack lands in order,
main would have taken the corrected wording in pr1 and then had it reverted
by pr5, leaving the shipped claim wrong again:

- all six hosts' pause skill back to "a minimal anonymous telemetry ping"
- docs/integrations/deepseek-plugin.mdx back to "Anonymous usage events"
- integrations/zapier-mem0/README.md back to advertising telemetry the app
  does not have, with an MEM0_TELEMETRY opt-out that controls nothing
- the README data-dir listing back to omitting telemetry-salt and
  install-state.json, both of which this stack creates
- the README and docs property lists back to the enumeration that drifts

Restored verbatim from pr4. No file here is in pr5's scope, and the full
pr4..pr5 diff is now surface headers only.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-15 22:37:16 +05:30
PowderXu b51f7692f0 docs(upstash): correct the default collection namespace (#7287) 2026-09-15 19:41:46 +05:30
jianyx1 dc7f88363f docs: fix Hermes integration page to match the current plugin (#7244) 2026-09-15 19:23:17 +05:30
Saket Aryan e9cbc626c0 fix(pi-agent): widen the search options by one property instead of to never
CI caught what I could not check locally: `pnpm exec tsc --noEmit` fails with

    error TS2353: Object literal may only specify known properties,
    and 'source' does not exist in type 'SearchMemoryOptions'.

Adding `source` to SearchMemoryOptions in mem0-ts does not help here. pi-agent
resolves `mem0ai` from npm, so it typechecks against the published 3.1.8 types,
not this repo's source. The declaration still belongs in mem0-ts for the next
release; this call site needs to compile today.

Widened by exactly that one property rather than restoring `as never`, which
was the original objection: a blanket cast also disabled checking of filters,
threshold, topK and rerank on the same literal. `source` reaches the wire
through the SDK's camelToSnakeKeys spread either way.

Verified by installing the package deps and running the real gates: tsc clean,
build clean. vercel-ai-sdk typechecks clean too. Also carries the spool-test
environment fix that had not been committed in this worktree.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-15 00:42:44 +05:30
Saket Aryan 3b43c78f7b Merge branch 'pr3/spool-delivery' into pr4/install-marker-and-identity 2026-09-15 00:37:46 +05:30