feat(clients): identify the SDK with a User-Agent header - #1561
Merged
Merged
Conversation
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
erni-a
force-pushed
the
feat/clients-user-agent
branch
from
September 18, 2026 06:07
d497d4e to
e479677
Compare
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
erni-a
added a commit
that referenced
this pull request
Sep 19, 2026
…e it (#1587) ## Why AI answer engines cite pypi.org 506 times in our GEO benchmark (27 May to 11 Sep 2026); Mem0 has 45 attributed PyPI pages, Caura has 0. Engines read only the release metadata and the rendered long description. Checked against the PyPI JSON API on 18 Sep: `caura` and `caura-sdk` have a 4-line description, no keywords and no classifiers; `caura-client` opens with a retirement blockquote and leaked `<!-- legacy-name-… -->` markers and never says what Caura is beyond one line. ## What changes Every Caura-owned PyPI page now opens with the same header block (what Caura is, the former name, MCP/REST/Rail access, tenant isolation, visibility scope and trust level), and all three packages share one summary style, keyword set, classifier set and project-URL set (Homepage, Documentation, Source, Issues, Changelog, Benchmark → LongMemEval harness). - **`caura`** becomes the product page: install, a 60-second write/search/recall quickstart, the MCP client config block, the three ways agents use memory (Rail, MCP/REST, Interviewer), links. Under a screen and a half. - **`caura-sdk`**: same page with its own install line; import package stays `caura_sdk`. - **`caura-client`**: header block, one-line rename notice, then the API README. Strict-writes (422) notes move below the API sections; Benchmark and "what Caura is" links go above the API table. - **Publish workflows** strip the legacy-name lint markers from `README.md` before `python -m build` and fail if any HTML comment survives. The source keeps the markers so the rename ratchet still accounts for each mention; the published long description no longer carries them. `memclaw-client` is intentionally untouched: 0.5.0 is yanked and its source was removed in #1244. ## Not in this PR No version bumps. Metadata only reaches PyPI on a release, so once this is merged a follow-up bump (`caura` 1.0.2, `caura-sdk` 1.0.1, `caura-client` 1.0.3) plus the `caura-meta-v*` / `caura-sdk-v*` / `caura-client-v*` tags ship it. Merging #1561 first keeps the client version file and README in one place. ## Verification - `python -m build` + `twine check dist/*` + `python -m readme_renderer README.md` on all three packages with markers stripped: PASSED, built METADATA has 0 HTML comments, first description line is the header block. - `scripts/legacy_name_ratchet.py --base origin/main`: no new lines (2 exempt lines removed, 4 floor mentions and 3 aliases listed). - `tests/test_client_metapackages.py`: 18 passed. - `clients/python`: `ruff check src tests` clean, `pytest` 150 passed. Done-when check after release, from any shell: ``` curl -s https://pypi.org/pypi/caura/json | python -c "import json,sys; i=json.load(sys.stdin)['info']; print(i['summary']); print(i['keywords']); print(i['project_urls']); print(i['description'][:400])" ``` Signed-off-by: erni <erni@caura.ai>
erni-a
added a commit
that referenced
this pull request
Sep 19, 2026
…1619) Version bumps only, so the PyPI page work from #1587 reaches the index. Metadata changes only reach PyPI on a new release. | Package | Directory | Version | Tag to push after merge | |---|---|---|---| | `caura` | `clients/caura-meta` | 1.0.1 → 1.0.2 | `caura-meta-v1.0.2` | | `caura-sdk` | `clients/caura-sdk-meta` | 1.0.0 → 1.0.1 | `caura-sdk-v1.0.1` | | `caura-client` | `clients/python` | 1.0.2 → 1.0.3 | `caura-client-v1.0.3` | The `caura-client` bump also updates `__version__` in `caura_client/__init__.py`. If #1561 (SDK User-Agent header) merges first, this PR needs a one-line rebase to set the version in the `_version.py` it introduces instead, and 1.0.3 then ships the User-Agent header too. If this merges first, #1561 ships in a later 1.0.4. Each publish workflow checks the tag against `pyproject.toml` and then confirms PyPI serves the version. Signed-off-by: erni <erni@caura.ai>
erni-a
force-pushed
the
feat/clients-user-agent
branch
from
September 19, 2026 14:21
e479677 to
b2e0af8
Compare
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Eldad-Caura
previously approved these changes
Sep 19, 2026
Both caura-client packages now send a User-Agent naming the package, its version and the runtime, so a self-hosted Caura server can count which SDK families talk to it (the heartbeat's clients_24h buckets). Nothing else is added to any request and no request goes anywhere new; the header is the whole change. caura-client-python/<version> (python/<major>.<minor>) caura-client-node/<version> (node/<major>) The Python version literal moves to caura_client/_version.py so client.py can import it without a cycle; __init__ still exports __version__ for the meta packages. The TypeScript client gains src/version.ts (no version constant existed in source) plus a test that it agrees with package.json, and dist/version.* joins the published files. Node detection is guarded so a browser bundle does not crash. READMEs document the headers each client sends. Signed-off-by: erni <erni@caura.ai>
erni-a
force-pushed
the
feat/clients-user-agent
branch
from
September 19, 2026 14:40
b2e0af8 to
625e33e
Compare
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Eldad-Caura
approved these changes
Sep 19, 2026
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Merged
erni-a
pushed a commit
that referenced
this pull request
Sep 19, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>backend: 3.17.0</summary> ## [3.17.0](backend-v3.16.3...backend-v3.17.0) (2026-09-19) ### Features * **clients:** identify the SDK with a User-Agent header ([#1561](#1561)) ([42e537b](42e537b)) * **core-api:** send an anonymous daily heartbeat from self-hosted servers ([#1577](#1577)) ([35ef4ca](35ef4ca)) * **plugin:** identify the plugin with a User-Agent header ([#1578](#1578)) ([3cdb2f7](3cdb2f7)) ### Bug Fixes * **clients:** wrap network failures in the SDK error hierarchy ([#1588](#1588)) ([99ee1ea](99ee1ea)) * **events:** make a failed publish visible to the platform, not just the SDK ([#1617](#1617)) ([f26e45d](f26e45d)) * **storage:** name the phase when cross-link discovery times out ([#1620](#1620)) ([7d3fbcb](7d3fbcb)) ### Dependencies * update alembic requirement from <2,>=1.19.1 to >=1.20.0,<2 ([#1569](#1569)) ([3f07940](3f07940)) * update google-cloud-aiplatform requirement from <3,>=2.0.1 to >=2.1.0,<3 ([#1570](#1570)) ([132981b](132981b)) ### Documentation * **clients:** make the PyPI pages answer what Caura is and how to use it ([#1587](#1587)) ([f5e4c5c](f5e4c5c)) ### Code Refactoring * **storage:** drop three zero-caller service methods, and gate the topic registry ([#1618](#1618)) ([1c206e0](1c206e0)) </details> <details><summary>plugin: 2.23.0</summary> ## [2.23.0](plugin-v2.22.3...plugin-v2.23.0) (2026-09-19) ### Features * **plugin:** identify the plugin with a User-Agent header ([#1578](#1578)) ([3cdb2f7](3cdb2f7)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: release-please[bot] <release-please[bot]@users.noreply.github.com> Co-authored-by: caura-deploy-bot[bot] <265395343+caura-deploy-bot[bot]@users.noreply.github.com>
erni-a
added a commit
that referenced
this pull request
Sep 19, 2026
…ly on a bad collector URL (#1623) ## Summary End-to-end validation of the heartbeat shipped in #1577 (backend 3.17.0) found that a Docker container sends two beats per cycle, that a mistyped collector URL is announced as ON and then refused silently, and a handful of smaller reporting and documentation defects. This PR fixes all of them in one place, and bumps the TypeScript client so the User-Agent header from #1561 can be published. ## Defects and fixes **D1 - two heartbeats per container.** `core-api/Dockerfile` runs uvicorn with `--workers 2`; every worker runs the lifespan, so each started its own loop. Observed against production on 2026-09-19: deployment `0e9d4f29...` POSTed at 16:23:52Z and 16:23:59Z, deployment `1f866780...` at 16:28:55Z and 16:29:21Z (both 202; the receiver's 20 h dedupe hid the second). `GET /api/v1/telemetry` answered differently depending on which worker took the request (`last_sent_at`, `next_send_at`, `payload_preview`), and the in-process `clients_24h` counter was split, so the stored beat carried only the sending worker's half (one worker previewed `caura-client-python "21-100"`, the other `"0"`). Fix (no new dependency, worker count unchanged): a per-container state directory (`CAURA_TELEMETRY_STATE_DIR`, default `<tmpdir>/caura-heartbeat`, created 0700) and a new `core_api.heartbeat.state` module. - Leader election: a non-blocking `fcntl.flock` on `<dir>/leader.lock` taken in `install()`. The holder runs the send loop; the others run no loop and print a "follower" line instead of a second ON line. Followers retry the lock every 60 s and take over if the leader dies (the kernel drops the lock), resuming from the leader's published `next_send_at` (bounded by one interval). - Shared client counter: every worker flushes its in-memory family counter every 30 s and on shutdown to `<dir>/clients-<pid>.json` (write-to-temp + atomic rename). At send time the leader sums its live counter with every other live worker's file; on 202 it zeroes its own counter and writes `<dir>/epoch`; a worker seeing a newer epoch at its next flush subtracts what it had already flushed. Files of dead pids are pruned (`os.kill(pid, 0)`). - Shared status: the leader writes `<dir>/state.json` after each cycle step; a follower answers `GET /api/v1/telemetry` from it, so every worker reports the same values. `payload_preview` is built by the answering worker from the summed counters. - Unwritable directory: WARNING at boot and fall back to today's one-loop-per-worker behaviour; never a crash. Empty variable: same fallback, by choice, at INFO. **D2 - plain-http collector URL announced ON, refused silently.** `check_endpoint_url` ran only in `send_once`, at DEBUG, so the boot line said ON, `GET` said `enabled: true`, and nothing was ever sent or explained. The check now lives in `policy.evaluate()`: an invalid URL is `Disabled(reason="invalid_endpoint_url")`, the boot line prints OFF with the reason and the offending URL at WARNING, and `GET` shows `enabled: false`, the reason and the endpoint. The send-time check stays as defence in depth. **D5 / docs.** README said "`CI` set to any value"; code and `docs/telemetry.md` say non-empty (an empty `CI=` keeps the heartbeat on). README now says non-empty. **Small fixes.** - (a) Delivery failures (connection refused, timeout, non-202) left `last_status`/`last_sent_at` null with no trace. The state and `GET` gain `last_error` (short, URL-stripped, so a credential in an override can never surface) and `last_attempt_at`; failures are logged once per cycle at WARNING instead of DEBUG. - (b) `CAURA_VERSION=v3.17.0`, the pinning form `docs/self-hosting.md` recommends and which core-api reads from `.env`, was sent verbatim. The payload `version` and the `User-Agent` now strip a leading `v`. - (c) `docs/telemetry.md` now states: one beat per container regardless of worker count (and how), source runs report `dev` unless `CAURA_VERSION` is set (and that the collector excludes `dev`), `providers.*` report the configured kind even when the server fell back to the fake provider, the new `invalid_endpoint_url` row, `last_error`/`last_attempt_at`, and the new boot lines. README kept consistent. `.env.example` documents `CAURA_TELEMETRY_STATE_DIR`. **TypeScript client 1.0.2.** The published `@caura/client` (1.0.1) predates the User-Agent header from #1561, so its requests are counted as `other`. `clients/typescript` is bumped to 1.0.2 (`package.json`, `package-lock.json`, `src/version.ts`; the `VERSION agrees with package.json` test passes). Publishing needs tag `caura-client-ts-v1.0.2` on the merge commit (publish-npm-client.yml). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## How Has This Been Tested? New tests: - `tests/test_heartbeat_state.py`: lock acquisition and hand-over (including a real child process that dies holding the lock), counter-file aggregation, epoch reset, stale/malformed/dead-pid files, `state.json` round trip. - `tests/test_heartbeat_multiworker.py`: two `multiprocessing` worker processes sharing a state directory against a fake receiver; asserts exactly one POST per cycle over two cycles, that the beat carries the summed counts (`1 + 1 -> "2-5"`, then `"0"` after the epoch reset), identical status from both workers, `v3.17.0 -> 3.17.0`, and that shutdown releases the lock. Runs in about 5 s. - Extended `test_heartbeat_sender.py` (roles, takeover, `last_error`, WARNING once per cycle, URL-free error text), `test_heartbeat_policy.py` (URL row, boot lines, `install()` role election and the unwritable-dir fallback), `test_api_telemetry.py` (new fields, a follower worker's `GET`, the bad-URL response), `test_heartbeat_payload.py` (version normalisation). Gates, run as in `ci.yml` from a fresh `uv venv -p 3.12` with all four packages editable: `ruff check` + `ruff format --check` on `core-api/src` and `tests/`; `cd core-api && mypy src/`; `scripts/tenant_scope_gate.py --base origin/main`; `scripts/legacy_name_ratchet.py --base origin/main` (no new lines); `pytest tests/ -m "not benchmark"` against a `pgvector/pgvector:pg16` container with CI's env after `alembic upgrade head`; `clients/typescript`: `npm install` + `npm test` (33 pass). Docker check: built `core-api/Dockerfile` from this branch and ran it through `docker-compose.yml` (the real CMD, two uvicorn workers) with a receiver sidecar sharing the container's network namespace on `127.0.0.1:8010`. Boot log: one `anonymous heartbeat ON` line (worker pid 10) and one `anonymous heartbeat follower` line (worker pid 9), where 3.17.0 printed two ON lines. Six `GET /api/v1/telemetry` calls spread over both workers returned the same `deployment_id` and `next_send_at` (`2026-09-19T17:16:29Z`); the receiver saw exactly one POST, at `17:16:29Z`, whose `clients_24h.caura-client-python` was `6-20` (those six calls, summed across both workers). After the beat both workers reported `last_sent_at 17:16:29Z`, `last_status 202`, `last_error null` and the same `next_send_at`. The state directory held `leader.lock`, `clients-9.json`, `clients-10.json`, `state.json` and, after the send, `epoch`. ## Checklist - [x] I have read CONTRIBUTING.md - [x] I have added tests that cover my changes - [x] `ruff check` and `ruff format --check` pass - [x] `mypy` passes - [x] `pytest` passes locally - [x] I have updated relevant documentation (README, docs/telemetry.md, .env.example) - [ ] CHANGELOG.md is generated by release-please from the squash title ## Additional Notes - Separate containers (replicas) still each send; the collector's 20 h dedupe per deployment covers that case, as before. The lock is per container because `/tmp` is. - `docs/telemetry-schema-v1.json` is unchanged: no payload field was added or renamed. Signed-off-by: erni <erni@caura.ai>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Both
caura-clientpackages now send aUser-Agentheader naming the package, its version and the runtime. A self-hosted Caura server can then count which SDK families talk to it, which is what theclients_24hbuckets of the Caura Heartbeat v1 spec (client PRs C1/C2) need. Nothing else is added to any request and no request goes anywhere new: the clients still talk only to thebase_url/baseUrlyou configure. The header is the whole change.Headers
caura-client-python/<version> (python/<major>.<minor>), e.g.caura-client-python/1.0.2 (python/3.12)caura-client-node/<version> (node/<major>), e.g.caura-client-node/1.0.1 (node/20); in a browser bundle the Node tag is omitted (andfetchdrops the header anyway, which is fine)Changes
clients/python: version literal moves tocaura_client/_version.pysoclient.pycan import it without a cycle;__init__still exports__version__(thecaura/caura-sdkmeta packages import it).USER_AGENTconstant added to thehttpx.Clientdefault headers. Tests assert the header on every request and that__version__agrees withpyproject.toml.clients/typescript: newsrc/version.ts(no version constant existed in source), re-exported asVERSION;USER_AGENTconstant added to the request headers withprocess.versions?.nodeguarded throughglobalThis.dist/version.js/dist/version.d.tsadded to the publishedfiles. Tests assert the header and thatVERSIONequalspackage.json's version.Testing
Run exactly as
client-python-ci.ymlandclient-typescript-ci.ymldo:clients/python:pip install -e ".[dev]",ruff check src tests(all checks passed),pytest -q(148 passed)clients/typescript:npm install,npm test(tsc + node --test, 20 passed);npm pack --dry-runconfirmsdist/version.jsanddist/version.d.tsship in the tarball