The supported-versions section still said "released and at v1.0.0" and
listed all of `1.x` as supported, which had drifted two minor releases.
It is now stated as a line ("1.2.x (current)") rather than a pinned
patch number, so it does not go stale on the next release.
Also:
- Point readers at /security/advisories, and tell them to read the
affected range on the advisory rather than comparing version numbers.
- Add a threat-model bullet for ingested documents. The loopback-only
default covers who can call the API, but filenames, metadata, and
content arriving from elsewhere are untrusted regardless of how the
API is reached.
- Note that reporters are credited in the advisory as well as the
release notes, which is what we do in practice.
Reporting stays email-only; GitHub private vulnerability reporting is
deliberately left disabled so the documented 5-business-day response
runs through one monitored inbox.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 1.1.4 section now describes the code shipped as everos==1.1.4 on
PyPI, which lists the path traversal as fixed. Read top-down, that left
a 1.2.0 user concluding the fix predates their version and they are
safe — the opposite of the truth, since 1.2.0 was built from a branch
that never received those fixes.
Three changes, no history rewritten:
- The 1.2.1 Fixed entry now names what 1.2.0 regressed, instead of
describing the merge as internal branch hygiene.
- A Security section under 1.2.1 carries the affected-version range,
which is discontinuous: everything before 1.1.4, plus 1.2.0. 1.1.4
itself is not affected.
- A note under 1.2.0 points forward, so a reader who looks up their own
version rather than the newest one still finds it.
Matches the published v1.2.1 release notes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kendrick-Song <61358070+Kendrick-Song@users.noreply.github.com>
* feat(examples): add zero-install Langfuse trace replay
Native OTel moved span emission into the server, so the Langfuse example
lost its try-before-install path: seeing anything now required a
configured EverOS. Restore one without fabricating spans.
replay.py pushes a recording of a real EverOS run into the reader's own
Langfuse project. Names, attributes, token usage, structure and durations
are replayed verbatim; only ids, timestamps and a `replay` tag are
rewritten, so nothing in the trace is invented. It needs the OTel SDK and
Langfuse keys, nothing else.
record_trace.py is the maintainer tool that produced the recording. It
stands in for Langfuse's OTLP and scores endpoints on localhost, which
works because EverOS derives both from langfuse_host, so one sink captures
both signals straight from a real server run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW
* feat(examples): give the Langfuse demo a memory worth searching
The demo ingested one conversation and searched it, so recall had nothing to
choose between and the traces showed plumbing rather than behaviour.
Eleven short conversations now span ten weeks, each on its own topic, so a
question has to find the right memory in a populated store. Two revisit the
same subject five days apart, close enough for geometry clustering to group
them, which finally gives reflection something to consolidate: the demo nudges
reflect_episodes (a `0 2 * * 1` cron otherwise), waits for the merge to land,
and the superseded memory is gone from search by the time the questions are
asked. One question asks about something never discussed, so a miss looks like
a miss.
KEYWORD is no longer a demonstrated method. Its top score is raw BM25, on a
different scale from the calibrated ones, so showing the three side by side
invited a comparison that means nothing.
Readiness is polled per session rather than slept through, since a fixed sleep
searched a half-built index and reported scores lower than the memory deserved.
Polling is deliberately slack: every probe is itself a traced search, and a
tight loop buried the real questions under a wall of readiness checks.
recorded_trace.json is that run against 1.2.1: 237 spans over 60 traces, no
errors, no secrets, synthetic content throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump version to 1.2.1 and finalize CHANGELOG. Highlights: [embedding]
and [rerank] become soft runtime dependencies; new everos cascade
backfill CLI; LanceDB schema v2 (nullable vector); PyPI Trusted
Publishing workflow; 1.1.4 backport fixes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(config): make [embedding] and [rerank] soft dependencies
Make [embedding] and [rerank] soft runtime dependencies so a
freshly-onboarded user can run EverOS end-to-end with only [llm]
configured. Previously the server refused to start without embedding,
locking out anyone who just wanted keyword-only search.
## Capability tiers
- Tier 1 ([llm] only) KEYWORD search, add/flush, md
writes, cascade sync
- Tier 2 ([llm] + [embedding]) + VECTOR / HYBRID search,
reflection, skill extraction,
backfill
- Tier 3 ([llm] + [embedding] + rerank) + AGENTIC search, knowledge
Tier upgrades require a server restart (capability accessors cache
for the process lifetime). Tier downgrades are read-safe: a Tier-3
user who drops [rerank] can still read/rename/delete existing
knowledge documents; only write/search endpoints return 422.
## What changed
- Component accessors — component/{embedding,rerank,llm}/accessor.py
are the single process-wide provider singletons. service/* never
maintains parallel singletons; it consumes get_embedding_capability()
/ get_rerank_capability() / get_llm_client() directly. Build-time
ValueError from the factory is logged as capability_build_failed
(was silently swallowed).
- Error mapping — ProviderNotConfiguredError -> 422 with everos.toml
section hints (never EVEROS_* env-var strings).
LanceDBMigrationError fails loud with escalating recovery guidance
(restart -> wipe index). LLMNotConfiguredError in search maps to
None for KEYWORD degradation.
- Nullable-vector LanceDB migration — schema v2 makes the vector
column nullable so Tier-1 rows can land without embeddings.
Migration is guarded by a cross-process memory_root_lock
(fcntl.flock + anyio.to_thread) and runs optimize() per table
after Phase-1 backfill to reclaim manifest bloat.
- Cascade — knowledge handlers register unconditionally (Tier-3 ->
Tier-2/1 downgrade no longer strands DELETE); embed-requiring
strategies use body-guards that check capability.available at
execution time. _TABLE_SPECS has an import-time drift assertion
against BUSINESS_SCHEMAS_WITH_VECTOR.
- `everos cascade backfill` CLI — Phase-1 (embed missing vectors) /
Phase-2 (emit synthetic events for cascaded processing) / Phase-3
(sync new skill files). Exit codes: 0 / 1 / 2 / 3 (server running
preflight) / 4 (COMPLETED_WITH_FAILURES — per-row failures rolled
up) / 130 (SIGINT). OMEConfig.crash_recovery_enabled=False in
backfill engines prevents stale-RUNNING rows re-enqueuing into a
smaller strategy registry.
- /health — reports capabilities + disabled_features per tier so ops
can distinguish "boots but degraded" from "boots and full".
- Presentation split — memory / service / infra never import typer /
click. TyperPresenter Protocol + run_backfill() live in
entrypoints/cli/commands/_backfill_cmd.py. Enforced by
import-linter.
- Startup hint — unconditional count_rows(filter="vector IS NULL")
sweep emits unbackfilled_memory_rows (event name + hint text
pinned) when Tier-1 rows exist. ParserLifespanProvider warms the
everalgo.parser import at boot so /health doesn't block on first
call.
- Knowledge upload UTF-8 short-circuit — _looks_like_utf8_text()
routes text/* mime and known plaintext extensions (md/txt/rst)
straight to UTF-8 decode instead of the parser. Prevents 503
Multimodal-not-configured when Tier 3 sans [multimodal] uploads a
markdown doc.
## Sync history with main (2 merges collapsed into this squash)
Merged origin/main at 6dcd3eb (v1.1.4 -> v1.2.0 adds OTel tracing,
/api/v2 alias, TracingLifespanProvider, per-cascade-embedding span
fix, memory-op instrumentation) and later at 42629df (PR #366
backfills v1.1.4 CWE-22 knowledge path traversal fix + cascade
retry-budget rework + errors.py -> core.errors.ExternalServiceError).
Key merge decisions:
- service/search.py adopts single wrap site — component.llm accessor
already applies UsageRecordingClient when observability is on;
service layer never keeps a parallel LLM singleton (Round-1 CR
rule: "service layer never maintains parallel singletons").
- Knowledge router prefix moved to /knowledge; create_app() mounts
it under both /api/v1 and /api/v2.
- Cascade retry classification uses ExternalServiceError from
core.errors (cascade/errors.py deleted). _MAX_TOTAL_RETRIES=12
cross-cycle budget preserved.
- Fixed backport typo: MemoryRoot.default() -> MemoryRoot.resolve()
(no .default() classmethod exists — main PR #366 shipped a broken
call).
## Verified layering
$ git grep -l "^import typer\|^from typer" src/everos/{memory,service,infra}
# empty
$ git grep -l "^import click\|^from click" src/everos/{memory,service,infra}
# empty
Memory / service / infra layers clean of CLI presentation libraries.
## Review history
Three rounds of Fable 5 (opus) code review across the pre-squash
commit history closed 38 findings total:
- Round 1: 10 findings (fail-loud migration, backfill hardening,
knowledge router gate scoping, SearchManager guards, profile
throttle lift)
- Round 2: 13 findings (hermetic test env, hot-reload doc drift,
knowledge handler registration, Phase 3 sync guarantee, Phase 2
idempotency, profile event-first path, OMEConfig crash-recovery
gate, cross-process migration lock, batch embed per-row fallback,
LanceDB optimize, typer/click layer split)
- Round 3: 15 Minor cleanups (accessor unification, marker revert,
episode query hygiene, --verbose subcommand, parser lifespan warm,
task-number scrub, temporal-overlap test, real-SIGINT slow mark,
4 design-note back-references)
Full per-round context lives in the PR description on GitHub.
## Test plan
- make lint (ruff + import-linter 3 contracts + assets +
deprecated-names + github-docs + datetime + OpenAPI drift)
- Hermetic env full pytest — 2027 passed / 7 deselected (7 = slow +
live_llm markers)
- Manual e2e across Tier 1/2/3 (21/21 assertions across v1/v2
double-mount and Tier 3 -> Tier 2 downgrade)
- /health reports correct capabilities + disabled_features per tier
## Known follow-ups
- .superpowers/sdd/followup-http-bridge.md (gitignored) — Path A for
spec §10's "backfill 期间 EverOS 完全可用" promise
- _TABLE_SCHEMA_VERSION docstring — v3+ migrations need a version
dispatch table
- extract_user_profile.py throttle-counter block — replace LanceDB
count_by_owner with a sqlite memcell count
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(review): close 3 blockers surfaced by round-4 review
N1. cluster_repo.find_cluster_id_for_member was cross-owner-unsafe.
Its reverse index (member_type, member_id) alone cannot disambiguate
two owners whose entry_id happens to collide — entry_id is
deliberately only per-owner unique (see entries.py:47:
'Cross-user uniqueness is handled at the database layer via a
composite <user_id>_<entry_id> field; it is not encoded into the
EntryId string itself'). Phase 2's _scan_all_rows crosses all
owners, so on any multi-owner root, same-day seq=1 episodes under
different owners would either false-hit each other's cluster or
be silently skipped from clustering. Add required (app_id,
project_id, owner_id) keyword args + JOIN Cluster (which already
carries scope) to filter by parent scope. Prior signature had zero
production callers except two the same PR just added, so the
API break is contained. Regression test: two owners persist a
cluster each around the same entry_id, each lookup resolves to
its own owner's cluster, a third owner's lookup returns None.
N2. Ctrl-C / EOF at the y/N prompt was landing on the generic
except Exception branch (exit 2 with rich traceback) instead of
the exit-130 interrupt path. Root cause: typer 0.15+ vendored
click under typer._click, so typer.Abort and the standalone
click.exceptions.Abort are distinct classes. The interrupt-branch
catch only listed the standalone one; every existing 'abort'
test was manually raising click.exceptions.Abort so the miss
was a false-positive guard rail. Widen the catch to
(typer.Abort, click.exceptions.Abort) and declare click as a
first-class dependency (it was only pulled in via uvicorn).
Regression test: raise real typer.Abort() at the confirm step
and assert exit 130 + INTERRUPTED banner.
N3. _looks_like_utf8_text used mime.startswith('text/'), which
caught text/html as well. HTML uploads then bypassed everalgo's
_aparse_html — losing clean_html_for_llm (strips <script>/<style>
/<nav>/<iframe> + HTML comments) and the 1 MiB output cap. A
40 MiB .html with <script> bodies and <!-- prompt injection -->
comments would flow straight into the extraction LLM. Replace
with explicit allowlist {text/plain, text/markdown, text/x-rst,
text/x-markdown}; text/html and any future text/* mime now
default to the parser path. Test matrix asserts text/html →
False (was regressed as True by the earlier commit).
Hermetic env full pytest: 2033 passed / 7 deselected (+6 tests
from these regressions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(review): close round-4 major + minor + PR body errata
Round-4 review-driven cleanup. Blocker fixes (N1/N2/N3) landed in
561b5fe. This commit closes the remaining CONFIRMED items:
Major:
- J3 MemoryRoot.default() -> resolve() was a breaking public API
rename this branch introduced (main still has default()). Adds
default() as a backward-compat alias forwarding to resolve()
with DeprecationWarning; CHANGELOG entry under Unreleased.
- J4 episode_repo.list_by_owner_after_ts(limit=N) truncates in
fragment order (== insertion order), NOT newest-first. Docstring
now spells out the trap so a future caller passing limit for a
'newest N' window doesn't silently get the oldest N.
- J5.2 TyperPresenter.nothing_to_backfill picked colour via
'could not be read' in message — a domain wording change
would silently flip yellow -> green. Signature gains explicit
scan_failed: bool kwarg; CLI colour-picks off the flag.
- J5.5 phase_header was Protocol-declared but never dispatched
(run_backfill calls _print_phase_header directly). Removed the
dead Protocol method + both no-op implementations.
- J6.3 3 inline from everos.core.errors import ... inside
Phase 1/2/3 preflights promoted to a single top-level import.
- J7 subject-side embed failure was silently exit-0 because
rows_processed advanced whenever any side wrote. Now: a row with
a needed side still NULL counts as rows_failed (exit 4 =
COMPLETED_WITH_FAILURES). Gated on spec.subject_of + row.needs_*
+ row.subject_text so non-Episode tables and subject-empty rows
don't false-positive.
- J9 test_migration_cross_process.py did NOT actually test cross-
process (all 5 tests mock memory_root_lock). Renamed to
test_migration_lock_wiring.py; docstring now scopes it to
'lock-invocation wiring' and points at test_core/…/test_locking.py
for real flock coverage.
- J10 Phase 1 lacked the server-running preflight Phase 2/3 have.
--phase all against a live server would burn Phase 1 embed API
calls (real cost) before Phase 2 halted with exit 3. Phase 1 now
probes _probe_ome_lock_available first; regression test in
test_backfill_preflight.py; upgrade_path integration patches the
probe so its in-process 'server + backfill' scenario stays valid.
Minor:
- M1 knowledge upload with NUL byte or filename > 255 bytes UTF-8
used to raise ValueError/OSError at write_bytes → 500 with a
half-written md left on disk. _safe_original_filename now
rejects both up front with InvalidInputError (→ 400).
- M2 backfill optimize() now passes cleanup_older_than=timedelta(0)
so older manifest versions are physically pruned (previous call
compacted fragments but left the manifest chain on disk).
- M3 verify_business_schemas remediation text used to jump straight
to 'rm -rf ~/.everos/.index/lancedb'; now walks restart → wipe.
- M5 multimodal/accessor.py capability_build_failed warning added
so all four provider accessors log symmetrically (was silent).
- M7 test_knowledge_api parser-absence tests call
parser_available.cache_clear() around the sys.modules patch so
the lru_cache doesn't strand a stale True/False.
- M8 cascade_handler_embed_skipped (6 handlers) demoted INFO → DEBUG:
Tier 1 imports were generating N × 6 handler-info lines per md.
- M10.1 test_drift_scenario_would_raise was a tautology (compared
two hardcoded string sets, never touched the guard). Now
monkey-patches BUSINESS_SCHEMAS_WITH_VECTOR to a superset and
reloads _backfill, proving the import-time RuntimeError fires.
- M11 test_cascade_verbose_position subprocess.run calls gain
env= — scrubs EVEROS_* from the developer environment so the
same footgun as round-2 B1 doesn't re-appear inside subprocesses.
- M14 health() -> dict degraded the OpenAPI schema to
additionalProperties: true. Introduce HealthResponse +
HealthCapabilities Pydantic models so clients get real field
shape; docs/openapi.json regenerated.
- M16 routes/knowledge.py:_require_knowledge_capabilities docstring
claimed cascade.registry.build_handlers still gates
knowledge_topic/knowledge_document, contradicting
registry.py:177-194 (gate removed there, moved to HTTP layer).
Rewritten to describe the current design accurately.
Hermetic env full pytest: 2037 passed / 7 deselected.
Explicitly deferred to followup:
- J1 lazy multimodal client (needs everalgo signature change)
- J2 tier definition (knowledge = Tier 3 whole)
- J5.1/3/4 broader presentation-split refactor
- J6.1/2 backfill dispatch and _backfill_table refactor
- J8 Phase 1 keyset pagination for bulk migration OOM
- M4 flock timeout/waiting log/re-entry (core/persistence refactor)
- M6 UTF-8 codec strategy (BOM/UTF-16/GBK)
- M9 count_by_owner monotonicity (latent, INTERVAL=1 short-circuits)
- M13 --phase all multi-phase combined-outcome test coverage
- M15 PR-marker rationale comments (16 occurrences, all inert)
M12 was refuted (both event names exist).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1.2.0 introduced /api/v2 as the canonical, cloud-aligned prefix and mounted
every business router twice, but the user-facing entry points (README,
README.zh-CN, QUICKSTART, the docs/ set, the Langfuse example) still taught
/api/v1 — so new users were pointed at the compatibility alias while
docs/api.md already declared v2 canonical.
- Switch every EverOS endpoint reference in docs, examples, and
`everos demo --live` to /api/v2, plus the matching CLI test expectations.
- Describe /api/v1 as a legacy compatibility alias that may be removed in a
future major release, rather than a permanent one. Nothing changes at
runtime: both prefixes still resolve to the same handlers and the
v1/v2 parity test is untouched.
- Add a short note in README / README.zh-CN / QUICKSTART so existing v1
integrations know they keep working.
- Fix the five dead endpoint anchors in the docs/api.md table of contents,
which still pointed at the pre-1.2.0 #post-apiv1... slugs.
Left on v1 deliberately: docs/migration-to-1.0.0.md (historical record),
CHANGELOG history, tests/** (v1 must stay covered), and the
use-cases/claude-code-plugin + openher READMEs, which document a different
cloud API.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Settings resolves its TOML source through resolve_root(), so any field a
test does not pass explicitly was filled from the real everos.toml on the
machine running the suite. Enabling [observability] locally, for instance,
made test_returns_singleton_when_configured fail, because the LLM client
picks up the usage-recording wrapper when tracing is on: green in CI, red
on that developer's machine, and unrelated to whatever they were changing.
An autouse fixture now points EVEROS_ROOT at a per-test tmp dir. Tests that
exercise root resolution itself already setenv / delenv inside the test
body, which runs after the fixture, so none of them needed changing.
Claude-Session: https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: zhanghui <huizhang1995@gmail.com>
Langfuse aggregates scores by name, so one name may only carry values on
one scale. recall_top_score was emitted for every method, mixing HYBRID's
LR-sigmoid probability and AGENTIC's cross-encoder score (both comparable
in [0, 1]) with KEYWORD's unbounded BM25 and single-route VECTOR's cosine.
A chart on that name averaged the two scales, and in practice a keyword
score can read numerically higher than a calibrated one while meaning less.
Uncalibrated methods now report recall_top_score_raw, leaving
recall_top_score comparable across methods and over time. Every recall
score also carries metadata = {method, calibrated}: a structured field
Langfuse persists and can split on, which the free-text comment could not
serve. The comment stays for reading individual scores.
Breaking for anyone charting recall_top_score for keyword search; 1.2.0 is
four days old, so this is the cheapest moment to correct the naming.
Claude-Session: https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub had no publish path — the package was previously released from the
(now-archived) GitLab mirror. Bring publishing to the public repo:
- .github/workflows/release.yml: on a vX.Y.Z tag, build + smoke-test
(make package) and upload to PyPI via Trusted Publishing (OIDC, no stored
token), gated behind the `release` environment for manual approval. A guard
step refuses to publish when the tag != pyproject version.
- .claude/skills/release: /release documents the cut (bump version →
CHANGELOG → tag → approve → verify) and the one-time PyPI trusted-publisher
+ GitHub environment setup.
Requires two owner-only one-time steps before the first release (documented
in the workflow header and the skill): register the GitHub trusted publisher
on PyPI, and create the `release` environment with required reviewers.
Co-authored-by: zhanghui <zhanghui@shanda.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(knowledge): contain original-file write path (CWE-22)
The multipart upload filename was joined into ``_original/`` verbatim.
An attacker-controlled filename such as ``/tmp/pwned`` or
``../../.bashrc`` would let ``POST /knowledge/documents`` write outside
the document directory (the ``/`` operator discards the left operand
for absolute paths; ``..`` walks upward). The read side had the
symmetric issue.
Fix, mirroring the sender_id containment shipped in 1.0.1
(GHSA-c795-2g9c-j48m):
- Add ``_safe_original_filename`` reducing the untrusted filename to a
single POSIX/Windows-basename component; reject degenerate residuals
(``""``, ``"."``, ``".."``) with PathTraversalError.
- ``_write_original_file`` asserts ``target.resolve()`` stays inside
``original_dir.resolve()`` before any filesystem touch (mkdir/write).
- ``_resolve_original_file_path`` sanitises symmetrically so a stored
provenance label can never resolve to an out-of-directory file.
Four SEC regression tests cover: absolute filename, ``..`` traversal,
degenerate filename rejection, and read-side sanitisation.
Backport from GitLab release/v1.1.4 (commit 40f19de) — 1.1.4 shipped
this fix; the GitLab -> GitHub sync stopped at 1.1.3, so 1.2.0
regressed the containment. This commit alone restores the fix; the
1.2.1 release PR ships it to PyPI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cascade): retry classification, budget, and reconcile races
Backport the cascade reliability work that shipped in GitLab 1.1.4
(MR !49 / commit 95db2f5) — six interlocking changes the reviewer
should read in order:
1. Worker retry classification uses ExternalServiceError (embedding /
LLM / rerank transient failures) as the "retry inline" signal.
The legacy RecoverableError hierarchy under cascade/errors.py is
removed; the retry contract now lives in the domain error tree
(core/errors.py). Docstrings in handlers/base.py and
sqlite/tables/md_change_state.py updated to match.
2. Cross-cycle retry budget: _MAX_TOTAL_RETRIES = 12. Once total
attempts across scanner cycles exhaust the budget, the worker
marks retryable=False in place instead of looping forever on a
sustained upstream outage.
3. md_change_state upsert preserves retry_count on scanner
re-enqueue when mtime is unchanged (previously reset to 0 every
sweep, defeating the budget). mtime change (user edit) still
resets the counter.
4. Reconciler no longer re-enqueues pending / processing rows on
stable mtime — that was overwriting the worker's mark_done. It
also skips failed rows with retryable=False on stable mtime so
the entry-check demote path is stable.
5. mtime tolerance (10 ms, MTIME_TOLERANCE_SECONDS) absorbs the
SQLite REAL float precision loss that previously flapped the
reconcile decision when the same md was rewritten without a real
content change. The constant is defined once in the sqlite repo
and imported by the reconciler so both sides use the same tol.
6. Worker _run_rebuild_once carries an explicit
`state.task is not asyncio.current_task()` guard before awaiting
the optimize task — the previous contextlib.suppress was silently
swallowing self-await RuntimeError.
Kept intact from the GitHub 1.2.0 baseline:
- The `except FileNotFoundError → handle_deleted` branch in the
worker (delete/modify race — see
test_modified_event_for_vanished_file_is_processed_as_delete).
Test coverage added:
- test_retry_budget_exhausted_marks_unrecoverable
- test_external_service_error_at_budget_edge_demotes_in_place
- test_upsert_preserves_retry_count_for_failed_stable_mtime
- test_upsert_resets_retry_count_on_mtime_change
- test_optimize_fallback_rebuild_on_sustained_failure
- reconciler mtime-tolerance / stable-mtime skip suite
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(embedding): raise on empty API data; forward MRL dimensions
Backport from GitLab 1.1.4 (MR !49 / commit 95db2f5).
Two behavioural changes on ``OpenAIEmbeddingProvider._embed_chunk``:
1. ``response.data == []`` now raises ``EmbeddingServiceError`` instead
of returning an empty list. Some upstream providers (observed on
DeepInfra under load) return HTTP 200 with an empty ``data`` array;
the silent zero-vector path was corrupting search indexes without
any signal.
2. When ``[embedding] dimensions = N`` is set in ``everos.toml``, the
parameter is forwarded to the API so MRL-capable models
(OpenAI text-embedding-3-*, Qwen3-Embedding, ...) do server-side
truncation with proper re-normalization. Client-side truncation to
``dim`` remains as a fallback for backends that ignore the param.
``openai.NOT_GIVEN`` is used as the sentinel so the request omits
the field when the setting is left at the default ``None``.
Config plumbing:
- ``EmbeddingSettings.dimensions: int | None = None``
- factory forwards ``dimensions=settings.dimensions`` to the provider
The provider stays inside the existing ``memory_span`` OTel wrapper
and continues to report input-only tokens via ``set_generation_usage``
- both are GitHub 1.2.0 native tracing behaviours preserved intact.
Test coverage:
- test_empty_response_data_raises_embedding_error (new)
- test_usage_span._FakeEmbeddings.create signature updated to accept
``dimensions`` kwarg so the OTel token-recording tests still exercise
the same call path
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(extract): retry episode extraction on malformed LLM output
Backport from GitLab 1.1.4 (MR !49 / commit 95db2f5).
The ``/flush`` synchronous path called ``EpisodeExtractor.aextract``
exactly once. everalgo raises ``ValueError`` when the LLM returns
malformed JSON (observed with OpenRouter partial responses where
finish_reason=stop but the body is truncated) — the caller was
surfaced a 500 for a transient upstream hiccup.
``_extract_with_retry`` wraps the call with two extra attempts at 1s
and 2s backoff (final attempt propagates untouched), typed as
``AlgoEpisode`` so the caller path stays annotated. Retry stays inside
the existing GitHub 1.2.0 ``memory_span("everos.extract", ...)`` OTel
wrapper — the OTel token capture and the retry loop are orthogonal.
TODO in the code notes we should catch a typed everalgo
``ExtractionError`` once that type is introduced (currently ValueError
is broad).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: log non-stop finish_reason; bump everalgo-user-memory 0.3.2
Two loosely coupled changes from GitLab 1.1.4 (MR !49 / commit 95db2f5)
that arrive together as a housekeeping commit.
1. ``_LoggingLLMClient`` diagnostic wrapper (new, file-private).
Wraps the raw everalgo LLM client and, on every ``chat()``, warns
when ``resp.finish_reason != "stop"`` — logging the reason,
``content_len``, the last 200 chars of ``content``, and ``model``.
Aims at OpenRouter/DeepSeek truncation triage where the provider
silently caps output length and returns finish_reason=length /
filter / etc. Non-invasive: one branch per call, no config gate.
Wrapper stack in ``get_llm_client``:
LoggingLLMClient(UsageRecordingClient(build_client(...)))
LoggingLLMClient(build_client(...)) # observability off
``UsageRecordingClient`` (GitHub 1.2.0 native OTel token capture)
stays gated by ``settings.observability.enabled`` — this commit
preserves that. LoggingLLMClient is always outermost so the reason
it observes is exactly the reason the underlying provider reported.
2. ``everalgo-user-memory`` 0.3.1 -> 0.3.2 (pyproject + uv.lock).
Same bump the GitLab 1.1.4 release lane took; unblocks the
episode-extract retry work in commit 4 seeing the upstream
improvements. Verified via ``uv sync``.
No functional API changes.
Test coverage:
- test_returns_singleton_when_configured now asserts the outer
LoggingLLMClient wrapper.
- test_wraps_client_when_observability_enabled asserts the two-layer
Logging(UsageRecording(...)) stack.
- test_does_not_wrap_client_when_observability_disabled asserts
Logging still wraps when tracing is off.
- test_logging_wrapper_warns_on_non_stop_finish_reason (new).
- test_logging_wrapper_silent_on_stop_finish_reason (new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): restore [1.1.4] section to match published sdist
The 1.1.4 changelog entry on this branch previously listed three
items (Langfuse example, delete/modify race, live-server telemetry).
The 1.1.4 sdist on PyPI, however, was built from the internal
release lane and includes the CWE-22 containment, the cascade
retry-budget / mtime-tolerance / reconcile-guard work, the embedding
empty-data raise, the episode-extract retry, MRL dimensions, and the
LLM finish_reason diagnostic — none of which were represented here
when the tag was cut.
Rewrite the [1.1.4] section so it matches the wheel a user actually
installs from PyPI:
- Add a header note explaining the retroactive restoration.
- Fixed: CWE-22, cascade reliability bundle, delete/modify race
(unchanged wording), embedding empty-data, episode extract retry,
Langfuse live-server telemetry (unchanged wording).
- Added: MRL dimensions, LLM finish_reason diagnostic, Langfuse
example (unchanged wording).
- Changed: everalgo-user-memory 0.3.1 -> 0.3.2.
The GitLab-side `.gitlab-ci.yml` in-house-runner entry is dropped —
open-source CI runs on GitHub Actions and the internal runner switch
is not visible to public users.
Date stays 2026-07-20 (the GitHub v1.1.4 tag date / PyPI upload
timestamp) rather than the internal 2026-07-23 code-freeze date, so
the timeline of what shipped where remains internally consistent.
The corresponding code fixes are all backported by earlier commits
in this PR; this commit only aligns the changelog surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minor release: `/api/v2` API prefix (v1 retained as alias) and native
OpenTelemetry tracing — both back-compatible, so 1.1.4 -> 1.2.0.
- pyproject: version 1.1.4 -> 1.2.0
- CHANGELOG: promote [Unreleased] to [1.2.0]
- docs/openapi.json + uv.lock: regenerated for the new version
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The examples/langfuse wrapper (everos_langfuse.py) was the interim client-side
instrumentation before EverOS gained native OpenTelemetry export. Now that
[observability] emits real OTLP spans, the wrapper is redundant and its faked
child spans could mislead. Replace it with a minimal, dependency-light example:
enable [observability] in everos.toml, run the server, and drive one
add/flush/search cycle (demo.py, stdlib only) to see native traces in Langfuse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Docs workflow (`links` job) is a required status check, but its
trigger was `paths:`-filtered to markdown/docs files. A PR touching only
code never triggered it, so the required check never reported and the merge
box stayed BLOCKED forever waiting for a status that would never arrive.
Drop the `paths:` filter: `make docs-check` validates the whole doc tree
independent of the PR diff and runs in seconds, so it is cheap to report on
every PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summarizes the OTel observability feature (instrumentation + the review /
telemetry-audit fixes) as one user-facing Added entry; the fixes themselves
targeted an unreleased feature so they need no separate lines.
The embedding-observation change wrapped every _embed_chunk call in a
span. Cascade-time indexing embeds run outside any request trace, so each
chunk started its OWN root trace — a per-chunk trace explosion (13 orphan
everos.embedding traces per add/flush), detached from session/user and
contrary to the "cascade is not instrumented" decision.
memory_span gains nested_only: open a span only when one is already
active. Embedding uses it, so search/flush embeds still nest under their
recall/extract span, while cascade embeds no-op (no trace) — restoring the
cascade-untraced boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the live-trace audit — all on the enabled path:
- boundary detection LLM (everalgo detect_boundaries) ran with the
SPAN-typed request root as the current span, so its ~1.2k tokens were
dropped from cost. Wrap it in an everos.memcell.boundary GENERATION
span so Langfuse prices it.
- embedding calls stamped usage on the enclosing retriever span. Wrap
each /embeddings call in an everos.embedding EMBEDDING span so the type
is correct and pricing can apply.
- agentic recall emitted a duplicate, same-name everos.search.recall
(cluster_scoped wrapping hybrid_full, which owns the real recall span).
Drop the redundant outer span; hybrid_full keeps the one recall span
(also used standalone in round 2).
- search now captures the returned hit ids (episodes/cases/skills) as
observation output when capture_content is on — previously only the
query input was captured.
- add/flush spans carry request_id in metadata.
- persist captures the memory-root-relative .md path, not the host
absolute path (no host layout leak to the telemetry backend).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the merged OTel instrumentation (#352) surfaced
four issues, all on the enabled path (default-off, so no impact until
tracing is turned on):
- search LLM client was unwrapped, so hybrid/agentic token usage — the
heaviest LLM spend — never reached Langfuse. Wrap it with
UsageRecordingClient when observability is enabled, mirroring
get_llm_client(); graceful keyword-only degradation is preserved.
- set_generation_usage overwrote token counts, undercounting any span
that wraps more than one chat call (the now-wrapped agentic path).
Accumulate instead of replacing.
- recall_hit was emitted for uncalibrated methods (unbounded BM25 /
single-route vector), a near-constant always-hit signal that inflates
dashboards. Gate hit on calibrated methods (HYBRID/AGENTIC); keyword
and vector emit only the raw top_score.
- init_tracing / init_score_sink were not idempotent — a re-init without
an intervening shutdown orphaned the export thread + OTLP socket +
worker task. Tear down the previous instance first (init_score_sink is
now async).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every business endpoint (memory/*, ome/*, knowledge/*) is now served
under /api/v2, aligning the open-source API with the EverOS Cloud
contract. /api/v1 is retained as a permanent, backward-compatible alias:
the same router objects are mounted under both prefixes, so both resolve
to identical handlers and request/response contracts. Existing /api/v1
integrations keep working unchanged. Infra endpoints (/health, /metrics)
stay unversioned.
Fix the Prometheus request-metric label to build the path from the full
request URL (with path params folded) rather than the route's
router-relative path, so the version prefix is preserved and v1/v2
traffic stays distinguishable.
Docs (docs/api.md, docs/openapi.json), CHANGELOG, and route docstrings
updated to lead with /api/v2. Add test_api_versioning as the parity
guard: every v2 route has an identical v1 twin and vice versa.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Open spans at the memory hot paths (all no-op when tracing is off):
- add / flush (service.memorize), extract + persist.markdown (user pipeline).
- search: everos.memory.search retriever + a uniform recall / rank
decomposition across keyword / vector / hybrid / agentic (manager, agentic
modules, cross-encoder callbacks); query-embedding tokens land on recall.
- recall quality: top_score / hit on the search span, plus recall_top_score /
recall_hit pushed to Langfuse scores via the bounded-queue sink (method
tagged; off the request path).
- OME: everos.ome.<strategy> agent span + everos.reflect.consolidate
generation; a W3C traceparent captured at enqueue is threaded through the
APScheduler job and re-attached in the Runner, so strategies fanned out
from a request nest under that request's trace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface gen_ai.* model + token attributes onto the active span so Langfuse
can compute cost — without touching everalgo:
- UsageRecordingClient wraps the LLM client and records response.usage after
each chat(); get_llm_client composes it over the existing _LoggingLLMClient
only when observability is enabled (disabled default stays overhead-free).
- OpenAIEmbeddingProvider records its response.usage (input tokens) onto the
active span too.
Tokens land on the everos.extract / everos.reflect.consolidate generation
spans and the search embedding recall; no-op when tracing is off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(examples): make Langfuse wrapper degrade cleanly on a real server
The wrapper synthesized child spans (extraction, embedding, hybrid
recall, rerank, index sync, consolidation) from a mock-only `_detail`
field. Against a real EverOS server that field is absent, so those spans
rendered with placeholder data — hardcoded model names, token=0, fixed
sleep durations — and recall scores fell to 0.
Now the per-stage child spans are emitted only when `_detail` is present
(the mock, or future native in-core instrumentation). Against a live
server only the top-level span per operation is emitted, with real
latency and output — no fabricated data. Recall quality
(recall_top_score / recall_hit) is derived from the real search
response, which already carries a per-hit score, so it works against a
live server today, not just the mock.
Verified: mock path unchanged (full trace tree, real scores); real-ish
path (no `_detail`) emits only top-level spans plus a real recall score.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(examples): count empty recalls as a miss in Langfuse hit-rate
When a search returns nothing scored, record recall_hit=0 (span attribute +
Langfuse score) instead of omitting it, so genuine empty recalls still show
up in recall hit-rate. No top_score is emitted (there is no hit to score).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds examples/langfuse/ — a thin OpenTelemetry wrapper that traces EverOS
memory operations (add / flush+extract / search / reflection) into Langfuse,
with recall quality pushed as Langfuse scores. Pure OTel SDK, no Langfuse
package dependency; runs against a built-in mock or a real EverOS server
(EVEROS_BASE_URL). Additive only, no changes to EverOS core.
Referenced by the upcoming Langfuse docs integration cookbook.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FTS indexes built with with_position=True crash lance's optimize/compaction
on lancedb >= 0.32 when merging an unindexed tail (Max offset exceeds length
of values; upstream lance-format/lance#7653). The crash aborts optimize()
including version cleanup, so the index dir grows unbounded until the disk
fills. everos recall is OR-mode BM25 and never does phrase queries, so
positions are never read -- disabling is lossless.
- base: default with_position=False
- infra: migrate_fts_indexes() rebuilds pre-fix indexes once at startup + reclaims orphans
- cascade worker: count consecutive optimize failures, escalate warning->error
Fixes#335.
Co-authored-by: zhanghui <zhanghui@shanda.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compile_filters() unconditionally appended 'deprecated_by IS NULL' to
every query, but the deprecated_by column only exists on user-scoped
tables (episode, atomic_fact — Reflection V1). Agent tables
(agent_case, agent_skill) lack this column, causing a SQL error on
agent search/get queries.
Gate the clause behind owner_type == 'user' so agent queries no longer
reference a non-existent column.
Bump version to 1.1.2.
Co-authored-by: Jiayao Song <jiayao.song@shanda.com>
* docs: re-link orphaned docs and slim engineering.md
After #311 realigned the docs, index.md again omitted four files that
exist under docs/: everos-demo, use-cases, migration-to-1.0.0, and
release-notes-1.1.0. Restore them so every docs/*.md is reachable from
the index, and rewrite engineering.md for an external audience.
index.md:
- Re-add a Tutorials section: everos-demo, use-cases
- See also: + release-notes-1.1.0, + migration-to-1.0.0
- Reframe the Engineering section as contributor-facing (not "internal")
engineering.md (575 -> 113 lines):
- Drop internal-only material: the self-justifying scope rationale, the
Claude Code loading internals, the infra failure-impact table, the
roadmap, and the "investing in infrastructure" essay
- Fix claims that were false for this GitHub repo: GitLab-primary CI,
the dev/master branch model, and the Gitmoji commit convention
- Keep what helps a contributor: toolchain, local make targets, the CI
gates, and the main-branch + Conventional Commits workflow
Conventions now match the repo's own .gitlint and GitHub Actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: align engineering.md references with the slimmed doc
Update the inbound descriptions of engineering.md now that it is a
contributor reference rather than an "infrastructure overview", and
repoint the one reference that named content the trim removed.
- CLAUDE.md, README, README.zh-CN, architecture.md: reword the link
text to "contributor engineering reference: build, test, CI, conventions"
- CLAUDE.md: the GitFlow Lite rationale pointer now targets
.claude/skills/new-branch/SKILL.md (engineering.md no longer carries it)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
index.md only linked 12 of the 19 docs under docs/. Readers entering
from the index missed configuration, multimodal, demo, use-cases,
benchmark, migration, and release notes.
- Add a Tutorials section (Diátaxis 4th quadrant): everos-demo, use-cases
- Reference: + configuration, + multimodal
- How-to: + locomo_benchmark, + migration-to-1.0.0
- See also: + release-notes-1.1.0
Every docs/*.md (excluding the openapi.json artifact) is now linked.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>