Repository navigation
EverOS 1.5.0
/add no longer has to wait for extraction, and an index can no longer be
wrong without saying so. A client can hand a batch to /add with
async_mode and get a 202 back in milliseconds, while a durable in-process
queue runs extraction in session order; the field, the response body and the
task-status paths match EverOS Cloud, so one client works against both. Vector
columns now follow [embedding] dim instead of a hardcoded 1024, startup
refuses an index the configured model cannot have produced instead of
accepting writes that are truncated or searches that quietly return nothing,
and an emptied index is reported instead of looking healthy. The rest is
reliability: OME writes no longer starve one another on Windows, a session
lock timeout no longer strands a memcell without its episode, a message the
model can never segment no longer blocks its session, resolving a cluster's
members no longer reads two embedding vectors per member, and a dozen smaller
faults found by property-based tests are fixed.
Added
-
async_modeon/add: accept now, extract in the background. A request withasync_mode: trueis written to a durable
SQLite queue (memorize_inbox) and answered with202 Accepted; an
in-process worker then runs the unchanged memorize pipeline for each
batch, per session in arrival order and several sessions at a time
(max_concurrency, default 4), so a session's batches and its flush
never overtake each other and one slow session does not hold up the
others. A synchronous call on a session that still has
queued batches is queued too, for the same reason;/flushhas no
async_modeof its own and queues only behind such a backlog. The queue is bounded
(413 PAYLOAD_TOO_LARGE,429 QUEUE_SATURATED), a batch whose delivery
fails is markedfailedand skipped (no automatic retry by default: the
pipeline is not atomic, so a re-run is not a clean retry), and every
row is deleted seven days after it was accepted.GET /api/v2/tasks/{task_id}
reports one request's outcome andGET /api/v2/taskslists them (filter by
session_id/status, paged). The field name, the202body, the task
paths and the status vocabulary match the EverOS Cloud gateway. Replays
after a crash are bounded (max_replays, default 3), the delivery loop
relaunches itself if it ever dies, andGET /healthreports the queue
(memorize_queueblock: loop alive, backlog, failed, restarts). Configured under
[memorize.async_queue];enabled = falsestops accepting new async
work while rows accepted earlier are still delivered. Synchronous calls
without a backlog are unchanged. -
[embedding] dimsets the width of every vector column. It used to be
hardcoded at 1024, so a model of any other width could not be used: a
narrower one failed every write, a wider one was truncated without saying
so.dimnow drives the schema, and startup checks the configured model
against it. (#434) -
cascade statusand/healthtell an emptied index apart from a healthy
one. Deleting.index/lancedbused to leave a store that reported perfect
health: the ledger lives in.index/sqliteand survives the delete, so every
file stayeddone, the scanner enqueued nothing,cascade statusshowed
done: 106withlag: 0, and/healthreported zero failures — while
search returned nothing. Startup now counts rows per business table and
compares them against the ledger.cascade statusprints a live count per
table and warns when every one of them is empty;/healthcarries the
startup snapshot incascade.index_rowsandcascade.index_emptied.
Reported, not enforced:healthydoes not flip, because these are startup
counts and a signal that cannot clear until the next restart is worse than a
number an operator can read. A fresh root, a queue still draining, and a root
whose markdown was all deleted are each quiet. Recovery iseveros cascade rebuild; markdown is the source of truth and is never touched. (#501, #522)
Changed
-
Startup refuses an index the configured embedding cannot have produced.
Two cases stop a start that used to proceed and fail later: the model
returns fewer dimensions thandim(nothing could ever be stored), and the
index records a different model than the one configured (distances
between vectors from two models are meaningless, and searches just return
nothing useful). Both name the values involved and point ateveros cascade rebuild; the CLI exits 3 without a traceback. Deployments with no embedding
configured (keyword-only, an emptyapi_key) are unaffected — the check is
skipped whole. -
An index records which embedding model built it. LanceDB keeps it in the
episodetable'svectorcolumn metadata, Milvus as a collection property.
An index that predates this release has no such record: the first start
after upgrading writes one in place, without touching a row or a column
type, and logs that it did.everos cascade rebuildrecords the model it
rebuilt with. -
/healthcapabilities.embedreports the startup probe, not whether a
provider can be constructed. It used to be true whenevermodel,
api_keyandbase_urlwere set, including against a dead endpoint. It is
now the result of the one embedding call made at startup, and does not flip
back without a restart. -
Every
everoscommand makes one embedding call at startup to measure
the model's real output width, with a 5-second budget and no retries. If it
fails, the start continues with a warning and the width check is skipped —
the model-change check does not depend on it.
Fixed
- OME: concurrent run-record writes could starve one another until the
busy timeout. Every store write opened its own connection and left the
waiting to SQLite's busy handler, which sleeps and retries, so under
several writers a late arrival could keep taking the write lock ahead of
an early one. On Windows, where a commit costs 30-80 ms, a writer starved
that way hit the 5 s timeout and failed withdatabase is locked
(measured on CI: 16 writers, p99 3.8 s, one failure in 30 s), which
silently drops the run when it lands on the RUNNING insert and leaves the
row RUNNING until a restart when it lands on a status update. Writers now
queue on an in-process lock that nobody overtakes before touching the
file. The engine also keeps one connection open for its lifetime: with
a connection per statement, every close used to be the last close, which
checkpoints and deletes the WAL files under an exclusive lock and on
Windows held a fresh connection's first statement past the same timeout. Found by the OME run state-machine test, which failed on Windows CI. - A message the model can never segment no longer blocks its session.
Boundary detection retries an unusable reply three times per call; a
message that always produced one stayed in the session's buffer, every
later/addor/flushmerged it back in and failed the same way, and the
session was stuck until someone cleared the buffer by hand (the async queue
marked that session's batchesfailedone after another). The third
consecutive call that ends this way now moves the session's buffer and the
call's own messages to thequarantinetrack ofunprocessed_buffer,
answers422 INVALID_INPUTnaming the session and the count, and the next
call starts from an empty buffer. Infrastructure failures (no quota,
network) are not counted and still propagate asEXTERNAL_SERVICE_UNAVAILABLE,
so a buffer waits out an outage. - A session-lock timeout could cancel extraction after the memcell was
written.memorize()wrapped the whole call in
session_lock_timeout_seconds; a timeout that landed after the boundary
had written the memcell row and consumed the buffer cancelled the
extraction pipelines, and nothing retried them: the cell stayed without an
episode for good. The timeout now bounds the two places a call can hang
while another waits on the lock, the lock wait and the boundary LLM call,
both of which run before any write; once a cell is committed its
extraction runs to completion, after the lock is released and shielded
from a cancelled request. Found by a property test that lands random
concurrent calls under random timeouts. - A replayed clustering run inflated the cluster's count. OME delivers a
strategy run at least once; whentrigger_profile_clusteringor
trigger_skill_clusteringran again for an episode or case it had already
folded in (a crash after the upsert, a retry after the emit failed), it
folded it in a second time: the member row was deduplicated butcount
rose and the centroid was re-weighted, andextract_user_profilegates on
that count. An entry already among the owner's members is now left where it
is; the downstream event is still emitted. - OME
inspectignored the Counter gate's cooldown. The dry run compared
the bucket's counter with the threshold only, so during a cooldown window
it reportedwill_run: truefor an eventdispatchwould hold back. The
store now answers the dry run with the same ruledispatchapplies
(threshold and cooldown, one clock). - Knowledge: a failed
PUT /knowledge/{doc_id}could leave two
directories for one document, or bury the old one inside the new. The
replace path backs the document's directory up, writes the new one and
restores the backup on failure; when the failure came after the writer
had created the new directory (the original-file step, for one), the
restore moved the backup inside it if the name was unchanged, or left it
next to the restored one if the title or category had changed. The
half-written directory is now removed before the backup comes back. - A hand-edited
entry_countcould make a daily-log writer reuse an entry
id. The episode, atomic-fact, foresight and agent-case writers allocated
the next id from the frontmatter'sentry_countalone, so lowering it by
hand made the next append produce an id the file already held (the index
keeps one of the two), and a non-numeric value raised aTypeError. The
next id is now allocated from the higher ofentry_countand the highest
sequence number already in the file, and a frontmatter that disagrees is
logged. - OME
wait_for_eventreturned while a retry was still pending. The
runner records a failed attempt asFAILEDbefore its backoff sleep and
gives the retry a fresh run id, so during the backoff the event's only row
isFAILED;wait_for_eventcounted that as terminal and returned.
Reflection waits on it before deprecating what the extraction replaced, so
it could act on an outcome that was not final.FAILEDnow means "retry
pending"; a failure with no retry left isDEAD_LETTER, as before. to_timestamp_msis now the exact inverse offrom_timestamp_ms. It
computedint(timestamp() * 1000), and the float product sat just below
the integer for about 0.6 % of instants (1095954883735 came back as
1095954883734); integer arithmetic on the timedelta since the epoch
replaces it.- The watcher classified md files the scanner never visits. Its kind
matcher was anchored on the right only, so a path with extra leading
components (x/app/project/users/u/episodes/episode-2026-01-01.md) was
accepted and queued while the root-anchored scanner glob ignored it; the
match now requires the same number of path components. default_app/default_projectare rejected as ids. They are the
on-disk directory names of the default scope, so an app with that literal
id shared the default app's directory and its index rows were attributed
todefault. The API now answers422for them.- Search: a duplicate id inside the dense pool kept its last score, not
its highest, and the multi-round decider's JSON parser gave up on a reply
with a stray{in the prose before the object (its docstring promised
otherwise); it now also decodes from every brace. - Cascade worker: a bounded wait between optimize and rebuild cancelled the
task it waited on. The optimize runner waits up to 180 s for an in-flight
index rebuild of the same kind, and the rebuild loop waits for an in-flight
optimize, so the two never commit on one LanceDB manifest at once. Both
waits usedasyncio.timeoutaroundawait task, which cancels the awaited
task when the deadline fires: a rebuild longer than the wait, with a write
landing on that table meanwhile, was killed mid-flight, and the rebuild loop
ended with it, silently, so no periodic rebuild ran again until restart and
that table's index UUID / FTS segment count grew unbounded. The mirror wait
cancelled the optimize beat it was meant to stay clear of. Both waits now
only stop waiting. Present since 1.2.2. - Cascade: the handler limit held per drain pass, not per worker. A worker
runs at most four handlers of a batch at once, but the limit was created
inside each drain pass, so two passes running together each got their own:
POST /cascade/quiescedrains while the background loop is still draining,
and that overlap peaked at eight concurrent handlers. The limit is now
shared by the worker. (#492) - Resolving a cluster's members read each member's full row, vectors
included. The profile path maps a fresh cluster's episodes back to their
parents and needed two fields per member, yet fetched every member's whole
episode row with both embedding vectors; a cluster can hold thousands of
members, so that read dominated the path's memory. LanceDB now reads only
parent_typeandparent_id(Milvus still loads full rows), and the
reflection path's bulk read skips the vector columns. (#490) method="vector"finds episodes when no atomic fact is in scope.
Vector search over episodes recalls through atomic facts, and it returned
an empty list (HTTP 200, no log line) whenever none were in scope — fact
extraction disabled, not caught up yet, or failing — while keyword search
kept answering. It now falls back to each episode's own vector, with
radiusapplied, and logsvector_recall_fact_pool_emptyat info.
(#502, #509)
Upgrade
pip install --upgrade everos # or: uv syncpip install --upgrade everos is enough for most roots, and nothing in this
release rewrites or re-embeds anything on its own. The first start records
which model built the index and logs one line saying so.
The first start also creates the memorize_inbox table in system.db for the
async queue. async_mode is opt-in: clients that never send it get the same
synchronous responses as before.
Two deployments need attention:
- A model narrower than
dim— the server now refuses to start instead of
accepting writes that could never be stored (this is #434). The error names
both numbers and the two steps: set[embedding] dim(or
EVEROS_EMBEDDING__DIM) to what the model returns, then runeveros cascade rebuild— the tables were already created at the old width, so
fixing the configuration alone is not enough. Your markdown is untouched
throughout. - A model wider than
dim— unchanged behaviour, but the start now warns
that the extra dimensions are truncated. The shipped default is this case
(Qwen/Qwen3-Embedding-4Breturns 2560,dimis 1024) and nothing is
wrong: that model is Matryoshka-trained, 1024 is one of its published
truncation points, and the index compares with cosine distance. Set
dimensions = 1024to silence it, if your endpoint accepts that parameter.
Changing dim or model later stops the next start and requires everos cascade rebuild, which re-embeds every entry from markdown — budget for it.
Downgrading to an earlier EverOS is safe as long as dim is still 1024:
the older schema check compares column types only and ignores the new record.
If you changed dim, set it back and rebuild before downgrading.
Full changelog: v1.4.2...v1.5.0