* feat(ome): exponential backoff + jitter between retry attempts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(events): carry case body + vector on agent skill chain
* feat(strategies): populate extended agent-skill chain events
* feat(md): AgentSkillReader.list_by_cluster for md-first skill enum
* fix(strategies): rescue extract_agent_skill from cascade-lag dead-letter
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agentic): honor radius, use kind-shaped rerank, non-empty passage
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(api/ome): expose dispatched + runs, distinguish not_dispatched
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): v1.2.3 — agent-skill rescue + OME/agentic contracts
* fix(agentic): drop inert radius plumbing; correct release docs
* fix(md): sanitize LLM-generated skill names against path traversal
AgentSkillFrontmatter.name comes straight from LLM output
(memory.strategies.extract_agent_skill) and was concatenated
unsanitized into the skills/skill_<name>/ directory segment on both
the write path (agent_skill_writer._skill_dir) and the read path
(agent_skill_reader._skill_dir). Given a sufficiently long ../ prefix,
the write target could escape the memory root (CWE-22). A live run
also produced a skill name containing spaces and CJK characters,
proving the "keep snake_case" docstring convention on
AgentSkillFrontmatter.name is not enforced at runtime.
This is the same class of defect already fixed for knowledge-upload
titles/categories. Promote that fix's sanitizer
(knowledge_writer._sanitize_dirname) to a shared primitive,
everos.core.persistence.markdown.sanitize_dirname, so there is one
CWE-22 defense for md directory names instead of two independently
maintained copies:
- New core/persistence/markdown/path_safety.py holds sanitize_dirname
(idempotent: sanitize(sanitize(x)) == sanitize(x)), exported through
the markdown + persistence facades.
- SkillPathMixin gains skill_dir_name(), the single sanitization point
both AgentSkillWriter._skill_dir and AgentSkillReader._skill_dir now
derive from, replacing their previous independent string
concatenation.
- KnowledgeWriter now imports the shared sanitize_dirname instead of
keeping its own private copy.
- AgentSkillFrontmatter.name gains a field_validator rejecting path
separators / ".." as defence in depth, so a hand-edited SKILL.md is
caught on parse rather than silently relocating the skill on the
next write.
Idempotency is what keeps the reader and writer in agreement even
though they recover a skill_name from different sources:
list_by_cluster derives it from the on-disk (already-sanitized)
directory name, while extract_agent_skill._hydrate_algo_skills
re-reads using the frontmatter's raw name field. A regression test
(test_agent_skill_reader.py) seeds a skill whose frontmatter name
contains CJK + a space and asserts both routes resolve to the same
file.
No data migration: agent-skill extraction has never once succeeded
before this branch (the cascade-lag defect this branch fixes meant
.skills/ was never created), so there is no legacy skill corpus whose
directory names would change under the new sanitizer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(e2e): make the agent-skill chain assertion real
tests/conftest.py's autouse fixture pins embedding + rerank capability
to unavailable for hermeticity, and this test never opted back in.
trigger_skill_clustering and extract_agent_skill both body-guard on
get_embedding_capability().available and return early, so the module
docstring's "real embedder" claim was false and the skill chain never
ran: measured log counts were skill_cluster_updated=0,
agent_skills_extracted=0, strategy_gated_off_embedding_unavailable=10.
The three skill assertions were assert len(...) >= 0 — always true —
with a comment blaming "LLM-dependent" flakiness for a count that was
in fact deterministically zero. This is why a defect that made
agent-skill extraction fail 4/4 in production reached a release: there
was no working e2e coverage of the chain.
- New _opt_in_real_embedding_and_rerank autouse fixture, scoped to this
file only, resets everos.component.embedding.accessor._capability and
everos.component.rerank.accessor._capability to None (the mechanism
the global fixture's own docstring prescribes) so both capabilities
rebuild from the real .env credentials tests/e2e/conftest.py already
loads. Restores to None on teardown; every other test keeps its
hermetic default.
- Replaced the three vacuous per-agent assertions with one aggregate
floor across all three agents (>= 1 total skill). A per-agent floor
would be flaky: extract_agent_skill has no cluster-size gate, only
everalgo's per-case skip_quality_threshold, so a single low-quality
trajectory can legitimately yield 0 skills for one agent.
- Added a sharper, defect-specific check: assert no dead-lettered
extract_agent_skill run in OME's run_record (via
OfflineEngine.list_runs), since a dead-letter (retries exhausted)
is unambiguously a failure, unlike a quality-gated 0-skill outcome.
- Corrected the module docstring's "real embedder" claim and the old
"# 4.5" comment's reasoning: extract_agent_skill has no cluster-size
gate, only everalgo's per-case quality threshold.
Unexecuted: this test is slow + live_llm and this machine has no
provider credentials (the verification .env was deleted), so make ci
does not run it and it could not be run here. Verified by inspection
instead: ran the test file with -m "" to override the marker
deselection and confirmed it proceeds past the new fixture and
through app lifespan startup without error, failing only at the
expected point — LLMNotConfiguredError from the missing API key —
which confirms the fixture and imports are wired correctly and the
only blocker is the missing credentials, not a bug in this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(strategies): sanitize skill name before frontmatter construction
Follow-up to afe1609 (path-traversal sanitization). That commit added a
field_validator on AgentSkillFrontmatter.name rejecting path separators
or "..", intended as a read-side defence for hand-edited SKILL.md
files. But _persist_skill (memory/strategies/extract_agent_skill.py)
constructs AgentSkillFrontmatter(name=skill.name) with skill.name
straight from LLM output — the raw, unsanitized string — so the
validator actually fires on the write path too. Sanitization already
makes the on-disk path safe (SkillPathMixin.skill_dir_name), so an
LLM emitting a traversal-shaped name (reachable via prompt injection,
since the LLM's input is user conversation content) gained nothing
from the validator except a new failure mode: ValidationError ->
strategy raises -> OME retries with backoff -> dead-letter -> that
case's skill is permanently lost. A DoS vector introduced by a
security fix, and it also contradicted the validator's own docstring
("catches a hand-edited SKILL.md").
Also fixes a latent second bug in the validator itself, caught by the
new tests below: it rejected any name containing the substring "..",
but sanitize_dirname keeps "." as a safe character, so
"../" * 8 + "tmp/pwned" sanitizes to "................tmppwned" —
still containing ".." many times over. The validator would have
rejected the sanitizer's own safe output. Narrowed the check to actual
path separators or the name being exactly ".." (the only case where
".." functions as a real traversal component when there's no separator
left to combine it with).
Fix:
- SkillPathMixin gains sanitize_skill_name(skill_name) — the bare
sanitized name (no skill_ prefix), factored out of skill_dir_name so
both share one sanitizer call.
- _persist_skill now sanitizes skill.name via sanitize_skill_name
once, up front, and uses that same sanitized string for
AgentSkillFrontmatter.id, .name, and the writer.write_main() call.
A traversal-shaped LLM name is now made filesystem-safe before it
ever reaches the frontmatter constructor, instead of tripping the
validator.
- The validator's docstring now describes actual behaviour: the write
path pre-sanitizes, so the validator only fires for a name that
bypassed the writer (e.g. a hand-edited file, or any other direct
AgentSkillFrontmatter construction that skips pre-sanitization).
Bonus: with the write path pre-sanitizing, frontmatter.name becomes
byte-identical to the directory-derived name for LLM-written skills —
an identity, not merely an idempotency argument. This also closes the
gap in the previous commit's reader/writer test, which proved
idempotency generically but never drove an adversarial name through
the actual production write path end-to-end.
Tests:
- test_agent_skill.py: constructing AgentSkillFrontmatter with a
pre-sanitized adversarial name (mirroring _persist_skill's own call
shape) succeeds and yields a separator-free name; the read-side
rejection test for bypassed/hand-edited names is unchanged and still
passes with the narrowed check.
- test_agent_skill_writer.py: new parametrized identity test — for
both an adversarial and a CJK/space raw name, sanitize once, write
via that sanitized name, and assert frontmatter.name equals the
directory-derived name exactly.
- test_agent_skill_reader.py: docstring updated to clarify its
existing round-trip test now covers the bypass case (a caller that
writes via a raw, unsanitized name directly through the writer,
skipping _persist_skill's pre-sanitization) rather than the normal
production path, which is proven as an identity by the writer test
above.
- Existing test_extract_agent_skill.py strategy tests (snake_case
fixture names) are unaffected — sanitize_dirname is the identity
function for already-safe names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(md): reject degenerate sanitizer results, read globbed paths
Security review of 0c6820f found it did not close the DoS it was meant
to close: sanitize_dirname("../") returns ".." verbatim, because "."
is a safe character and is not stripped by the character-class filter
— only the leading "/" is removed. sanitize_skill_name("../") ->
sanitize_dirname("../") -> "..", and AgentSkillFrontmatter(name="..")
still raises ValidationError (name == ".." is exactly the case the
narrowed validator rejects). Same dead-letter DoS as 0c6820f, just a
shorter payload; the previous commit's tests only exercised the long
"../" * 8 + "tmp/pwned" payload, which happens to sanitize past the
fixpoint. The same fixpoint is a real one-level directory escape on
the knowledge path, which has no skill_ prefix to protect it:
Path(root) / sanitize_dirname("../", "Others") / "doc_123" resolved to
root/doc_123, skipping the category directory entirely.
Fix (path_safety.py): sanitize_dirname now falls back on "", ".", or
".." instead of only "". This one change closes both the skill
dead-letter DoS and the knowledge one-level escape, since both callers
already route through this single primitive. Also NFC-normalizes the
input before the character filter (unicodedata.normalize("NFC", raw)),
so an NFD-decomposed accented character (base letter + combining
mark, which is not \w) no longer silently loses its accent. Rewrote
the docstring, which previously claimed ".. sequences are always
stripped" (false — "." is explicitly a safe character) and "cannot
escape the directory it is concatenated into" (false for an unprefixed
caller before this fix); it now states what actually holds: no
separator survives, so the result is always exactly one path
component, and it is never "", ".", or "..".
Also fixes (per review, cheap and worth doing alongside):
- AgentSkillReader.list_by_cluster previously globbed skill_*/SKILL.md,
stripped the prefix to recover a name, then called read_main(name),
which re-derives (and re-sanitizes) the path from that name. Any
on-disk directory whose suffix was not already a sanitizer fixpoint
(e.g. "skill_My Skill", a raw space) re-derived to a path that
doesn't exist and was silently dropped. Since list_by_cluster is the
documented strong-consistency existence check, a dropped skill would
make the LLM emit add() for a skill that already exists, duplicating
it at the sanitized path and orphaning the original. Fixed by having
list_by_cluster read each globbed path directly (new _read_path
helper, shared with read_main) instead of round-tripping through a
recovered name — the reader never derives a path at all on this
route, which is a stronger guarantee than the idempotency argument
the docstrings previously leaned on.
- e2e test: made fixture ordering explicit — the embedding opt-in
fixture now takes _reset_embedding_capability_singleton and
_reset_rerank_capability_singleton as parameters so pytest's
dependency graph guarantees correct ordering, rather than relying on
collection order between conftest files. Added a positive
"extract_agent_skill actually ran" assertion (any status) before the
dead-letter check — without it, the dead-letter assertion alone is
vacuously satisfied by a strategy that never executed at all; it was
only meaningful before because the skill-count floor happened to run
first. Dropped the rerank capability opt-in and the module
docstring's "real reranker credentials" claim: nothing on the
agent-skill write path touches rerank, so opting it in only widened
the credential surface with no coverage benefit.
- CHANGELOG: corrected the validator description (rejects a path
separator or being exactly "..", not any string containing ".."),
and added the previously-missing user-visible fact that
AgentSkillFrontmatter.name and the agent_skill LanceDB primary key
now hold the sanitized name, not the raw LLM output.
Tests: parametrized the sanitize -> construct -> (write, for the
writer-level test) tests over a boundary family instead of one long
payload: "..", "../", "/../", ".", "./", "!!!" (empty), "a" * 200
(truncation), a CJK+space name, and the original "../" * 8 +
"tmp/pwned". Each case asserts the sanitized name is a single
component, is never "" / "." / "..", frontmatter construction
succeeds, and (writer-level) frontmatter.name is byte-identical to the
directory-derived suffix. New test_path_safety.py cases pin the
degenerate-fixpoint fallback directly, the knowledge-style unprefixed
one-level-escape repro, and NFC normalization. New
test_list_by_cluster_finds_skill_whose_directory_suffix_has_a_space
reproduces the exact list_by_cluster drop bug against a directory
written outside the writer entirely.
Explicitly not in scope (per review): the collision behaviour where
"fix django" and "fix_django" now map to the same directory is a real
product-decision question the reviewer is raising separately, not
touched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(md): record the skill-name collision trade-off
No behaviour change. Documents a product decision the coordinator made
explicit: sanitize_dirname is lossy, so distinct raw skill names can
collapse onto the same directory ("fix django" and "fix_django" both
become "fix_django"; "fix!django" and "fixdjango" both become
"fixdjango"; names differing only past the 50-char cap also collide).
Because AgentSkillWriter.write_main is a full-file replace and the
LanceDB primary key is f"{agent_id}_{sanitized_name}", a collision
means the later skill silently overwrites the earlier one, losing its
accumulated source_case_ids, maturity_score, and body.
This is accepted rather than mitigated: the LLM's add/update decision
for a skill is keyed on the name it sees, so a collision usually reads
as an intended update anyway; and adding a disambiguating suffix would
break the frontmatter.name == directory-suffix identity the
reader/writer seam (from 0c6820f) relies on.
- SkillPathMixin.sanitize_skill_name docstring now states the
collision consequence and the two reasons it is accepted, so a
reader does not have to derive them.
- sanitize_dirname's docstring gains one line: the function is lossy
and not injective; callers that need distinct outputs for distinct
inputs must disambiguate themselves. General primitive — the
knowledge path calls it too.
- CHANGELOG: added the collision consequence to the existing
path-traversal entry, next to the already-documented fact that name
/ the LanceDB key hold the sanitized value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(md): return skill bodies from list_by_cluster; correct docs
Security review of 1b8cf11 + add842d found the list_by_cluster fix was
incomplete: it stopped its own enumeration from dropping a skill whose
directory suffix wasn't a sanitizer fixpoint, but the caller
(_hydrate_algo_skills) still re-read each selected skill by
fm.name via read_main, which re-derives (and re-sanitizes) a path from
that name and drops it there instead. Reproduced on a real
filesystem: skill_My Skill/ enumerates fine, but
read_main("My Skill") re-derives to skill_My_Skill/ and misses. The
drop moved one layer downstream; existing_relevant_skills was empty
before and after the prior fix.
Fix: list_by_cluster now returns (frontmatter, body) pairs instead of
frontmatter alone, so the caller never needs a second, name-based
read. _select_existing_skills / _rank_skills_by_relevance updated to
carry (fm, body) tuples through selection; _hydrate_algo_skills is
deleted — the body is already in hand, so there's nothing left for it
to do. This closes the drop for real, removes the second disk read
(the 2n-read concern carried since Task 4), and makes "the reader
never derives a path" true end-to-end rather than true only for
list_by_cluster's own enumeration step.
New end-to-end regression test
(test_select_existing_skills... / test_existing_skills_reaches_llm_for_skill_whose_directory_has_a_space)
seeds a skill_My Skill/ directory directly on disk (bypassing the
writer) and runs the real extract_agent_skill strategy against it,
asserting the skill reaches existing_relevant_skills with non-empty
content — the property the previous commit's test docstring claimed
but the code didn't yet deliver. The reader-level regression test
gained the same body assertion.
Also, per review:
- path_safety.py: corrected the NFC docstring claim, which was wrong
for the ~1,082 Unicode composition-exclusion codepoints (e.g.
Devanagari क़/ख़, U+0958/U+0959) — NFC decomposes an
already-precomposed exclusion character instead of preserving it, so
the combining mark is stripped either way. Scoped the claim to
"best-effort for the common case", not a guarantee for every script.
New test pins this directly.
- Dropped the e2e test's unused _reset_rerank_capability_singleton
fixture parameter: the reviewer adjudicated the earlier instruction
conflict the other way — ordering is only meaningful between
fixtures that touch the same state, and this fixture never reads or
writes the rerank capability at all.
- Widened the skill-name collision documentation (SkillPathMixin.sanitize_skill_name,
CHANGELOG) beyond dropped-punctuation / space-collapse / truncation
to the larger case: every combining mark is non-\w and is stripped
regardless of script, so e.g. Devanagari "किताब" and "कताब" both
collapse to "कतब" (same for Thai tone marks, Hebrew niqqud, Arabic
harakat).
- Corrected the collision justification: because _persist_skill
sanitizes before frontmatter construction, the LLM sees the
already-sanitized name in existing_relevant_skills, so a colliding
raw name is an *affirmative* decision that two skills are different,
not a probable intended update. The decision to accept collisions
still stands, but on its real grounds: a disambiguating suffix would
break the fm.name == directory-suffix identity the reader/writer
seam relies on, and detecting-and-raising would reintroduce the
dead-letter DoS.
- Merged two consecutive "# -- Internals --" banners in
agent_skill_reader.py into one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(md): skip unparseable SKILL.md instead of failing the cluster
`list_by_cluster` only handled a missing file, so a ValidationError from
`_read_path` aborted the whole enumeration. That enumeration is what
feeds `extract_agent_skill` its existing skills, so one bad SKILL.md
starved every skill in the cluster and dead-lettered that cluster's
extraction on every subsequent run -- the permanent-failure mode this
md-first read path was introduced to eliminate. The write path already
pre-sanitizes to avoid exactly this; the read path was left open.
The trigger surface is the whole schema, not just the traversal
validator this PR added: `_read_path` validates the full
`AgentSkillFrontmatter`, so a field a later revision makes required
would take out every existing file at once. Reproduced both ways.
`read_main` still propagates -- a caller naming one specific skill needs
an error, since `None` already means "not created yet" and reusing it
for "exists but is corrupt" would let an upsert overwrite the damage.
Also moves this route's `OfflineEngine` import under TYPE_CHECKING. It
is used only to annotate `_summarize_runs`, and an eager import
contradicted the deferred `_get_engine` import a few lines below. Note
this saves nothing at startup today: `service.memorize` imports the
engine eagerly to construct it, so any app process pays the ~750ms
apscheduler cost regardless.
* docs(md): correct two docstrings, add the case-collision dimension
Three fixes to claims that did not match behavior:
- `_rank_skills_by_relevance` claimed no skill is silently dropped from
the prompt. The backfill loop is capped at MAX_SKILLS_IN_PROMPT, and
the function only runs when the cluster already exceeds that budget, so
skills beyond K are dropped by design. Reworded to what the backfill
actually guarantees: a lagging index cannot under-fill the prompt.
- `sanitize_skill_name` enumerated collision causes in detail but omitted
case, the dimension an LLM varies most freely. "Fix Django" and "fix
django" sanitize to two distinct names -- two LanceDB rows, but one
directory on a case-insensitive filesystem (macOS APFS, Windows NTFS
defaults), so the index advertises a name whose content was overwritten.
- The same docstring justified accepting collisions partly on a
disambiguating suffix breaking the `frontmatter.name` = directory-suffix
identity. It would not: writing "fix_django_2" into both keeps that
intact. Replaced with the real reason it is deferred rather than
dismissed -- it needs a collision probe and a case-folding rule.
Adds the knowledge-writer sanitization tests that were missing entirely:
swapping in the shared primitive changed NFD input ("Résumé" no longer
degrades to "Resume") and made a "." / ".." topic fall back. Knowledge
upload predates this PR, so unlike skills it has a corpus whose
directory names those first cases affect. Both tests verified red against
the 1.2.2 sanitizer.
* docs(changelog): scope the migration claim, record run_record growth
Three corrections to the 1.2.3 entry:
- "No data migration" was asserted for the whole sanitization change but
only holds for agent skills, which have no corpus because extraction
never succeeded. Knowledge upload predates this release and does have
one: NFD topics and `.`/`..` topics resolve to a different directory
now. Scoped the claim and spelled out both cases.
- Added the case dimension to the collision list, and replaced the
"disambiguating suffix breaks the name = directory identity" reason
with the accurate one -- it does not break it, it just needs a probe
and a case-folding rule, so it is deferred rather than rejected.
- Recorded that `SkillClusterUpdated` now persists a 1024-dim vector in
`run_record.event_payload`: ~0.8 KB to ~14 KB per record, ~14 MB per
strategy at the default 1000-record ring buffer. Operators sizing
ome.db need this number, and it was not stated anywhere.
* fix(strategies): ship extract_foresight disabled by default
The sender scan reads `m.role` off every memcell item, but only
ChatMessage carries it: ToolCallRequest has `sender_id` and no `role`,
ToolCallResult has neither. So any memcell holding a tool call raises
AttributeError before the first sender resolves -- correct on plain user
chat, guaranteed to fail on agent trajectories, where it burns its
max_retries budget and dead-letters on output nothing consumes today.
Flipped the decorator rather than `default_ome.toml`, because `everos
init` skips an existing `~/.everos/ome.toml` (init_cmd.py:85), so a
template edit would reach new installs only. The toml opt-in is left
working on purpose -- a chat-only deployment does get correct
foresights -- and documented in both the module docstring and the
template comment.
This is a stop-gap. The fix is per-episode extraction, like
atomic_fact, which needs an everalgo entry point that does not exist
yet.
The one test that used foresight as its UserPipelineStarted subscriber
now opts back in through that same toml key, so the opt-in path is
covered rather than worked around. It has to wait for the override to
reach the registry first: ConfigReloader.start() fires its initial load
as a task, so engine.start() returns before ome.toml is applied, and an
emit inside that window is judged against the coded defaults and
dropped by the enabled gate with no redelivery.
* fix(ome): hold engine_sem per attempt, not across the retry chain
The backoff this PR added slept inside the semaphore block, so a run
waiting to retry kept its concurrency slot. That turns a partial outage
into a total stall: with max_concurrent_runs slots and a 1s/2s/4s
backoff, enough simultaneously-failing runs park every slot in
asyncio.sleep and starve strategies that would have succeeded.
The cap exists to bound concurrent strategy work -- LLM calls,
embeddings, storage IO -- and a sleeping coroutine consumes none of it.
Backpressure on the failing work is intended; backpressure on everything
else is not. Semantics change is deliberate and stated in the docstring:
the cap still applies to execution, no longer to waiting.
The guard uses a single-permit semaphore so locked() is unambiguous, and
asserts a second waiter actually acquires -- locked() alone would pass on
an implementation that freed the slot but left waiters unable to take it.
Verified red against the previous structure.
* fix(strategies): reap the directory a renamed skill leaves behind
everalgo treats a name change as a first-class update: _apply_update
preserves prior.id while swapping the name, so _persist_skill wrote the
skill to a new skill_<new_name>/ and the old directory survived with the
same cluster_id.
That is not a cosmetic leak now. Since existing skills are read from
markdown rather than LanceDB, the orphan returns in the next run's
existing_relevant_skills as a duplicate of a skill the LLM already
renamed -- feeding exactly the add-instead-of-update full-replace clobber
this PR set out to close, once more per rename. Reconciliation keys off
skill.id, the only field that survives a rename (a fresh add mints a
uuid4 and can never match), and never deletes a name another emitted
skill just claimed.
Also in this pass:
- AgentSkillWriter.delete_skill, the one destructive operation here. It
fails closed: a directory it cannot resolve by the writer's own path
rule is left alone rather than targeted by anything looser.
- reference_name / script_filename now sanitized on both reader and
writer. They are appended after the skill_<name> segment, so
skill_dir_name never covered them. Zero callers in src/ today; closing
it before progressive disclosure wires them up.
- Agentic case rows with every passage field empty fall back to a
placeholder instead of raising ValueError in everalgo's _format_docs
and 500ing a whole search the row merely appears in.
- The retire op is documented as unimplemented rather than left implied.
aextract returns a flat list with no discriminator, so a retirement
arrives as an ordinary low-confidence skill and is written back like
any other. Honouring it means either giving an LLM confidence score
authority to delete the source of truth, or a retired flag that the
enumeration, cascade, and search all learn to filter on -- a design
decision, deferred.
- The embedding body-guard comment no longer claims to protect a local
embed call; this strategy stopped embedding when the vector moved onto
the event.
* fix(strategies): stop extract_foresight crashing on tool-call memcells
The sender scan read m.role off every memcell item, but only ChatMessage
carries it: ToolCallRequest has sender_id without it, ToolCallResult has
neither. The first tool call raised AttributeError before any sender was
resolved, so the strategy was correct on plain user chat and
dead-lettered every time on agent trajectories.
everalgo contracts for exactly this input -- user_memory/_render
.chat_messages says "the caller need not pre-filter; an
AgentMemCell-shaped MemCell is acceptable input" -- and every other
user-memory extractor gets that for free by delegating. This strategy was
the one place the filter was hand-rolled, and it was hand-rolled wrong.
It stays disabled by default, but for the correct reason: nothing in
EverOS reads foresights yet, so running it spends one LLM call per sender
per memcell on write-only data. The earlier justification (per-episode
extraction needs an everalgo entry point that does not exist) confused
extraction granularity with the crash; granularity is still open, the
crash was one line. Fixing it is what makes the documented ome.toml
opt-in actually usable.
The guard pins both directions: a pure agent trajectory extracts nothing
and never reaches the LLM, and a mixed memcell extracts for human senders
only -- an implementation that stopped raising but scanned tool-call
sender_id values would invent "agent" as a user.
CHANGELOG also records that the stale-index clobber is fully closed only
for clusters at or below MAX_SKILLS_IN_PROMPT; above it LanceDB orders
the markdown candidates, and the skill a lagging index omits is the one
written most recently.
* docs(changelog): fold the merged cascade work into the 1.2.3 entry
#392 landed on main with its entries under [Unreleased] and no version
bump. Since 1.2.3 ships that code, leaving them there would have the
release notes disclaim work the release contains. Merged section by
section into [1.2.3] and dated it to the actual release day.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .claude | ||
| .github | ||
| benchmarks | ||
| data | ||
| docs | ||
| examples/langfuse | ||
| scripts | ||
| src/everos | ||
| tests | ||
| use-cases | ||
| .env.example | ||
| .gitignore | ||
| .gitlint | ||
| .pre-commit-config.yaml | ||
| ACKNOWLEDGMENTS.md | ||
| CHANGELOG.md | ||
| CITATION.md | ||
| CLAUDE.md | ||
| CODE_OF_CONDUCT.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| Makefile | ||
| NOTICE | ||
| QUICKSTART.md | ||
| README.md | ||
| README.zh-CN.md | ||
| SECURITY.md | ||
| config.example.toml | ||
| pyproject.toml | ||
| uv.lock | ||
README.md
Why Ever OS
EverOS is a Python library and local-first memory runtime for agents and makers. It gives one portable memory layer across coding assistants, apps, devices, and workflows from day one. It stores conversations, files, and agent trajectories as readable Markdown, then syncs local SQLite and LanceDB indexes for fast retrieval and self-evolving reuse.
| Title | EverOS | Other Agent Memory Libraries |
|---|---|---|
| Markdown source of truth | ✅ Canonical .md files that are readable, editable, diffable, and Git-versioned |
❌ Usually API, vector, graph, dashboard, or database state |
| Direct file editing | ✅ Edit .md files; cascade watcher syncs |
❌ Usually SDK, API, dashboard, or backend update paths |
| Local three-part stack | ✅ Markdown + SQLite + LanceDB; no MongoDB, Elasticsearch, or Redis required | ❌ Often depends on managed services, vector DBs, graph DBs, or server stacks |
| User + agent tracks | ✅ User episodes/profile and agent cases/skills are separate first-class surfaces |
❌ Usually centered on chat history, profiles, entities, facts, or retrieval records |
| Orthogonal retrieval | ✅ Search by user_id, agent_id, app_id, project_id, and session_id |
❌ Usually app, namespace, tenant, thread, or graph scoped |
| Knowledge Wiki | ✅ Editable, source-backed Markdown knowledge pages with taxonomy, CRUD APIs, and topic search | ❌ Usually separate from memory, trapped in a dashboard, or not tied back to source files |
| Reflection | ✅ Offline memory evolution that merges episode clusters and refines profiles and skills between sessions | ❌ Usually retrieval-only memory with little background consolidation or long-horizon improvement |
Quick Start
Goal: play with the memory visualizer first, then start EverOS, write one real memory, and search it back.
0. Prerequisites
- Python 3.12+
- No API keys are needed for
everos demo. - To run the real server-backed memory flow, create two provider keys before
everos init:
| Capability | Provider | Used for | Fill these .env slots |
|---|---|---|---|
| Chat + multimodal | OpenRouter | LLM / MULTIMODAL |
EVEROS_LLM__API_KEY, EVEROS_MULTIMODAL__API_KEY |
| Embedding + rerank | DeepInfra | EMBEDDING / RERANK |
EVEROS_EMBEDDING__API_KEY, EVEROS_RERANK__API_KEY |
You can use other OpenAI-compatible providers by changing the matching
*__BASE_URL fields in .env.
1. Install
uv pip install everos
# or: pip install everos
2. Play With The Demo
Run this before configuring API keys or starting the server:
everos demo
The command asks for one memory and one recall question, then opens a full-screen terminal UI. This is an educational visualizer: it is hardcoded, local to the CLI, and does not connect to the EverOS server. Its job is to make the memory lifecycle visible: conversation -> memory sphere -> recall -> source proof -> confetti. See docs/everos-demo.md for the demo scope and TUI source layout.
The sphere moves through ingest, extraction, indexing, recall, source reveal,
and a confetti burst after the first memory lands. Press r to replay and q
to quit.
For the looping showroom view used in README media, run:
everos demo --cinematic
If your shell is not interactive, or you want a copyable preview, use:
everos demo --plain
3. Configure
Generate a starter .env file, then fill the four API key slots shown in the
generated comments. With the default setup, paste your OpenRouter key into the
LLM / MULTIMODAL slots and your DeepInfra key into the EMBEDDING /
RERANK slots.
everos init
# or, from a source checkout:
cp .env.example .env
everos init writes ./.env by default. Use everos init --xdg to
write ${XDG_CONFIG_HOME:-~/.config}/everos/.env instead.
4. Start EverOS
everos server start
Keep the server running, then open a second terminal and check it:
curl http://127.0.0.1:8000/health
Expected response:
{"status":"ok"}
everos server start searches for .env in this order: --env-file <path> →
./.env (cwd) → ${XDG_CONFIG_HOME:-~/.config}/everos/.env → ~/.everos/.env.
The endpoint stack is OpenAI-protocol compatible (OpenAI / OpenRouter / vLLM /
Ollama / DeepInfra) - override *__BASE_URL in the generated .env to point
at any of them.
Now make the demo real. In the second terminal, run:
everos demo --live
Live demo mode connects to the running server and performs the real
/health -> /api/v2/memory/add -> /api/v2/memory/flush ->
/api/v2/memory/search flow before opening the same memory sphere UI. Use
--server-url <url> if your server is not on http://127.0.0.1:8000.
5. Try Your First Memory
[!NOTE] Business endpoints live under
/api/v2. The older/api/v1prefix still resolves to the same handlers so existing integrations keep working, but it is a legacy alias that may be removed in a future major release — write new code against/api/v2.
Add a tiny conversation:
TS=$(($(date +%s)*1000))
curl -X POST http://127.0.0.1:8000/api/v2/memory/add \
-H 'Content-Type: application/json' \
-d "{
\"session_id\": \"demo-001\",
\"app_id\": \"default\",
\"project_id\": \"default\",
\"messages\": [
{\"sender_id\": \"alice\", \"role\": \"user\", \"timestamp\": $TS, \"content\": \"I love climbing in Yosemite every spring.\"},
{\"sender_id\": \"alice\", \"role\": \"user\", \"timestamp\": $((TS+10000)), \"content\": \"My favorite coffee shop is Blue Bottle in SOMA.\"}
]
}"
Force extraction for the local demo:
curl -X POST http://127.0.0.1:8000/api/v2/memory/flush \
-H 'Content-Type: application/json' \
-d '{"session_id":"demo-001","app_id":"default","project_id":"default"}'
Search it back:
curl -X POST http://127.0.0.1:8000/api/v2/memory/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "alice",
"app_id": "default",
"project_id": "default",
"query": "Where do I like to climb?",
"top_k": 5
}'
You should see the Yosemite memory in the response. If the result is empty on the first try, wait a moment and retry; Markdown is written synchronously, while the local index catches up in the background.
[!TIP] First memory unlocked. You just gave EverOS a fact, flushed it into durable Markdown-backed memory, and searched it back through the local index. That is the core loop. Want to see the source of truth? Open
~/.everosand inspect the generated Markdown files.
For annotated responses and the Markdown files EverOS creates, see QUICKSTART.md.
Optional: Ingest Multimodal Files
To ingest non-text content (image / pdf / audio / office documents)
through /api/v2/memory/add content items, install the optional
extra:
uv pip install 'everos[multimodal]' # or: pip install 'everos[multimodal]'
This pulls in everalgo-parser (with the [svg] bundle for SVG
support via cairosvg) and wires up the multimodal LLM client
(EVEROS_MULTIMODAL__* fields in .env, defaults to
google/gemini-3-flash-preview via OpenRouter).
Office document support requires LibreOffice as a system dependency.
The parser shells out to soffice (LibreOffice's headless renderer) to
convert .doc / .docx / .ppt / .pptx / .xls / .xlsx to PDF
before feeding the result into the multimodal LLM. Without LibreOffice,
office uploads return HTTP 415 with a clear error message; PDF / image
/ audio / HTML / email parsing is unaffected.
Install on the host before serving office documents:
brew install --cask libreoffice # macOS
sudo apt-get install -y libreoffice # Debian / Ubuntu
For Contributors
git clone https://github.com/EverMind-AI/EverOS.git
cd EverOS
uv sync # creates ./.venv and installs deps
source .venv/bin/activate # or prefix commands with `uv run`
everos demo --plain # try the local educational demo; no API keys needed
everos init # paste OpenRouter + DeepInfra keys into .env
everos --help
make test
Use Cases
Now that you have had your first successful EverOS moment, explore what people are building with persistent memory across agents, apps, and community integrations.
Use cases show what persistent memory makes possible in real products and workflows. Some examples are packaged in this repository; others point to external demos or integrations you can study and adapt.
Reunite - Find With EverOSParents describe what they remember. Children describe what they recall. Reunite uses semantic memory to surface the connections. |
Hive OrchestratorBrowser-native hive-mind for CLI coding agents - Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol. |
AI Coding Assistants With EverOSUniversal long-term memory layer for AI coding assistants, powered by EverOS. |
AI Data TechnicianAn agentic AI system that learns from scientist interaction to inspect, analyze, and classify high-dimensional time series data - with persistent memory that improves across sessions. |
Rokid AI Assistant With EverOSConnect to EverOS within Rokid Glasses enabling long-term memory for all of your smart activities. Coming soon |
Creative Assistant With MemoryCreative assistant with long-term memory, so your creative context stays available across sessions. Coming soon |
|
|
|
Earth Online Memory GameEarth Online is a memory-aware productivity game that turns everyday planning into a living quest log. |
Multi-Agent Orchestration PlatformGolutra presents a multi-agent workforce for engineering teams, extending the IDE model from a single assistant to coordinated agents. |
Your Personal Tasting UniverseRecord, visualize, and explore your tasting journey through an immersive 3D star map. |
EverOS Open HerBuild AI that feels. Open-source persona engine - personality emerges from neural drives, not prompts. Inspired by Her. |
Browser Agent For Personal MemoryRuminer brings persistent memory to a browser agent so it can carry personal context across web tasks. |
EverMem Sync With EverOSOne command to connect any AI coding CLI to EverMemOS long-term memory. |
|
|
|
MCO - Orchestrate AI Coding AgentsMCO equips your primary agent with an agent team that can work together to solve complex tasks. |
Study Buddy With Self-Evolving MemoryStudy proactively with an agent that has self-evolving memory. |
Alzheimer's Memory AssistantEmpowering individuals with advanced memory support and daily assistance. |
Memory-Driven Multi-Agent NPC ExperienceAn iOS sci-fi mystery game where players explore and uncover the truth. |
Mobi CompanionAn iOS app where users create, nurture, and live with a personalized AI companion called Mobi. |
AI Wearable With MemoryA context-native AI wearable that listens to everyday life and converts conversations into memory. |
|
|
|
Legacy OpenClaw Agent MemoryArchived pre-1.0.0 plugin reference. New integrations should use the current EverOS API. |
Live2D Character With MemoryAdd long-term memory to a real-time Live2D character, powered by TEN Framework. |
Computer-Use With MemoryRun screenshot-based analysis with computer-use and store the results in memory. |
Game Of Thrones MemoriesA demonstration of AI memory infrastructure through an interactive Q&A experience with A Game of Thrones. |
Claude Code PluginPersistent memory for Claude Code. Automatically saves and recalls context from past coding sessions. |
Memory Graph VisualizationExplore stored entities and relationships in a graph interface. Frontend demo; backend integration is in progress. |
Documentation
- docs/everos-demo.md — Demo scope and TUI source layout
- docs/how-memory-works.md — Markdown, SQLite, LanceDB, and recall flow
- docs/use-cases.md — Full use-case gallery and integration examples
- docs/engineering.md — Contributor engineering reference: build, test, CI, conventions
- docs/migration-to-1.0.0.md — Legacy API migration notes
- CHANGELOG.md — Release notes
- CONTRIBUTING.md — How to contribute
EverMind Ecosystems
EverMind is an open-source ecosystem for long-term memory, self-evolving agents, AI-native interfaces, and memory evaluation.
| EverMind Open-Source Ecosystem | |
|---|---|
| Memory Runtime | EverOS - the local memory operating system and research-backed runtime for agent and user memory. |
| Self-Improving Agent Harness | Raven - the self-improving agent harness that brings memory, proactivity, context control, and skill evolution into terminal-native agents. |
| Algorithm Engine | EverAlgo - stateless extraction, ranking, parsing, and memory operators that power EverOS. |
| Hypergraph Memory | HyperMem - hypergraph memory for long-term conversations, with its own benchmark-backed topic -> episode -> fact retrieval method. |
| Benchmarks | EverMemBench · EvoAgentBench - evaluation suites for conversational memory and agent self-evolution. |
| Long-Context Research | MSA - Memory Sparse Attention for scalable latent memory and 100M-token contexts. |
| Personal Memory Layer | EverMe - CLI and agent plugin suite for cross-device, cross-agent personal memory. |
| Developer Integrations | evermem-claude-code · everos-plugins - plugins, skills, and migration tooling for AI coding agents. |
Together, these repositories form EverMind's research-to-runtime stack: new memory methods, reusable algorithms, benchmark evidence, and practical agent integrations.
Contributing
Contributions are welcome across the whole repository: memory methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse Issues to find a good entry point, then open a PR when you are ready.
[!TIP]
Welcome all kinds of contributions 🎉
Help make EverOS better. Code, documentation, benchmark reports, use-case write-ups, and integration examples are all valuable. Share your projects on social media to inspire others.
Connect with one of the EverOS maintainers @elliotchen200 on 𝕏 or @cyfyifanchen on GitHub for project updates, discussions, and collaboration opportunities.
Code Contributors
License
Apache License 2.0 — see NOTICE for third-party attributions.
Citation
If you use EverOS in research, see CITATION.md.