Commit Graph

3 Commits

Author SHA1 Message Date
Saket Aryan 2c885fdcd7 fix(plugins): make code.install reachable, and stop pinging on every flush
Review found the headline fix inverted: code.install could never fire, so every
fresh install reported an upgrade and the two cohorts became indistinguishable —
strictly worse than the bug being fixed.

hook_runner reaches claim_install() only after cache_plugin_api_key() has
written `api-key` and EvidenceStore() has created `evidence.sqlite3` and its WAL
files. Asking "is the data directory empty" at that point always saw content.
The caller now snapshots emptiness at the top of the run, before anything
writes, and passes it in.

Also caught by review, all in the same file:

- claim_version_change was an unsynchronized read-modify-write, so several
  concurrently starting sessions each observed the old version and each recorded
  an upgrade. The first session after a version bump is exactly when a user's
  open agent windows all restart together. The transition is now claimed with an
  exclusive per-version sentinel.
- A crash between O_EXCL and the write left an empty marker, which disabled
  every future upgrade event on that machine: claim_install saw the file and
  claim_version_change could not parse it. An unparseable marker is now
  repaired.
- claim_install consumed the one-shot claim even under MEM0_TELEMETRY=false, so
  a user who opted out for their first sessions would never report install after
  opting in.
- Existing users have an email but no key fingerprint, so the fast path always
  missed and every flush paid an uncached /v1/ping/ — a 5s timeout each time for
  the offline users this stack keeps citing. Legacy rows now adopt the current
  key's fingerprint instead of re-resolving.
- A key that will not resolve (revoked, offline) kept attributing to the
  previous account's email, which is the bug this was meant to fix. It now falls
  back to the anonymous id.
- The anonymous id was never rotated, so once it had been merged into one
  account it was still offered as the alias for the next one. An alias naming an
  already-identified id is what could link two real people; it is now offered
  once.

The gap that let this ship was that no test drove hook_runner's session-start
path — the decision was only ever tested by calling claim_install() directly on
a directory nothing had touched. Adds subprocess tests that run the real
entrypoint: fresh install, exactly-once, and an existing data dir.

62 core tests, 203 host tests.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-15 00:31:52 +05:30
Saket Aryan 0d2b20c03d fix(plugins): count installs once, and re-resolve the email when the key changes
code.install counted upgrades and repeat sessions. Session start records install
whenever is_first_run() is true, and that only checked whether
telemetry-identity.json exists. Recording install does not create that file —
only the first successful flush does. So install fired for every 0.2.x user on
their first 0.3.x session (0.2.x never wrote the file, and the data directory
survives the upgrade), again for any session starting before that first flush
finished, and — this is the part that makes it unbounded rather than a race —
on every single session, forever, for anyone whose flush never succeeds. An
offline or firewalled user reported a new install every time they opened an
editor, which is exactly the population hardest to see in the data.

A dedicated install-state.json is now claimed with O_CREAT|O_EXCL at the moment
install is recorded, so two sessions starting together cannot both win, and the
marker is not coupled to identity. Deliberately not the identity file: writing
that from a recording process would race the sender, which writes it during
resolve_distinct_id, and overloading it is what caused this.

Upgrade detection keys on the data directory already having content. A fresh
install has an empty one; anything else predates this session. That is a firmer
predicate than looking for 0.2.x's venv/ and requirements.txt, which is a guess
about files another part of the plugin may or may not have written and only ever
works for this one upgrade. The version is stored in the marker so later changes
record code.upgrade with a real from_version.

A cached email outlived an API key change. resolve_distinct_id kept the first
email it resolved and never looked again, so switching to a key from another
account kept attributing events to the previous one. It now stores a fingerprint
of the key the email came from and re-resolves when the current key differs, and
falls back to the anonymous id when no key is configured rather than continuing
to attribute to an account it cannot verify.

The dangerous part is the alias. resolve_distinct_id's second return value
becomes a PostHog $identify with $anon_distinct_id, and aliasing one account
email to another merges two real person profiles irreversibly. The re-resolve
path returns no alias; aliasing runs anonymous to email only, and never
email to email.

One existing test asserted that is_first_run flips when the identity file is
written, which is the defect itself. Rewritten, along with coverage for atomic
claiming, upgrade detection and version changes.

Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
2026-09-15 00:18:38 +05:30
Kartik 73e7b8763a refactor(integrations): shared agent plugin runtimes and native adapters (#7203) 2026-09-08 23:32:25 +05:30