Skip to content

EverOS 1.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 13:38
· 2 commits to main since this release
87a8a6e

/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_mode on /add: accept now, extract in the background. A request with async_mode: true is written to a durable
    SQLite queue (memorize_inbox) and answered with 202 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; /flush has no
    async_mode of 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 marked failed and 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 and GET /api/v2/tasks lists them (filter by
    session_id / status, paged). The field name, the 202 body, 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, and GET /health reports the queue
    (memorize_queue block: loop alive, backlog, failed, restarts). Configured under
    [memorize.async_queue]; enabled = false stops accepting new async
    work while rows accepted earlier are still delivered. Synchronous calls
    without a backlog are unchanged.

  • [embedding] dim sets 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. dim now drives the schema, and startup checks the configured model
    against it. (#434)

  • cascade status and /health tell an emptied index apart from a healthy
    one.
    Deleting .index/lancedb used to leave a store that reported perfect
    health: the ledger lives in .index/sqlite and survives the delete, so every
    file stayed done, the scanner enqueued nothing, cascade status showed
    done: 106 with lag: 0, and /health reported zero failures — while
    search returned nothing. Startup now counts rows per business table and
    compares them against the ledger. cascade status prints a live count per
    table and warns when every one of them is empty; /health carries the
    startup snapshot in cascade.index_rows and cascade.index_emptied.
    Reported, not enforced: healthy does 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 is everos 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 than dim (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 at everos cascade rebuild; the CLI exits 3 without a traceback. Deployments with no embedding
    configured (keyword-only, an empty api_key) are unaffected — the check is
    skipped whole.

  • An index records which embedding model built it. LanceDB keeps it in the
    episode table's vector column 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 rebuild records the model it
    rebuilt with.

  • /health capabilities.embed reports the startup probe, not whether a
    provider can be constructed.
    It used to be true whenever model,
    api_key and base_url were 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 everos command 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 with database 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 /add or /flush merged 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 batches failed one after another). The third
    consecutive call that ends this way now moves the session's buffer and the
    call's own messages to the quarantine track of unprocessed_buffer,
    answers 422 INVALID_INPUT naming 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 as EXTERNAL_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; when trigger_profile_clustering or
    trigger_skill_clustering ran 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 but count
    rose and the centroid was re-weighted, and extract_user_profile gates on
    that count. An entry already among the owner's members is now left where it
    is; the downstream event is still emitted.
  • OME inspect ignored the Counter gate's cooldown. The dry run compared
    the bucket's counter with the threshold only, so during a cooldown window
    it reported will_run: true for an event dispatch would hold back. The
    store now answers the dry run with the same rule dispatch applies
    (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_count could 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's entry_count alone, 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 a TypeError. The
    next id is now allocated from the higher of entry_count and the highest
    sequence number already in the file, and a frontmatter that disagrees is
    logged.
  • OME wait_for_event returned while a retry was still pending. The
    runner records a failed attempt as FAILED before its backoff sleep and
    gives the retry a fresh run id, so during the backoff the event's only row
    is FAILED; wait_for_event counted 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. FAILED now means "retry
    pending"; a failure with no retry left is DEAD_LETTER, as before.
  • to_timestamp_ms is now the exact inverse of from_timestamp_ms. It
    computed int(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_project are 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
    to default. The API now answers 422 for 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 used asyncio.timeout around await 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/quiesce drains 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_type and parent_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
    radius applied, and logs vector_recall_fact_pool_empty at info.
    (#502, #509)

Upgrade

pip install --upgrade everos   # or: uv sync

pip 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 run everos 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-4B returns 2560, dim is 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 = 1024 to 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