Repository navigation
Description
everos server start hangs after N successful KB uploads — single asyncio event loop appears to deadlock during cascade + extract_atomic_facts + LanceDB write
Summary
When ingesting multiple KB documents in sequence (via /api/v1/knowledge/documents), everOS reliably hangs after a small number of successful uploads (2–5 chunks in our runs). /health starts returning 502/timeout, although the LLM call returns HTTP 201 for some uploads. The process never recovers and must be killed.
This appears to be a deadlock in the single asyncio event loop between:
- The HTTP request handler waiting on
extract_knowledgeto finish - The
cascade_workercallingextract_atomic_factson a freshly-written doc - Both eventually needing the same LanceDB write slot
- cascade_worker holding a
processingrow inmd_change_state, blocking its own retry chain
After a few successful chunks, the OME pipeline stops making forward progress even though llama-server reports requests_processing=0 and all upstream LLM calls have returned.
Why this matters
The user-visible failure mode is severe:
/healthstops responding → monitoring / MCP tool calls all hangkb-ingest.progressstops updating (the watchdog has no signal)- A new client POST
/api/v1/knowledge/documentshangs forever, never receiving a response - Process must be hard-killed; on restart, cascade rebuild (2 s) returns but the queue still has stale
processingrows from the prior run
Reproduction is trivial: upload more than ~5 KB documents back-to-back. The lockup happens on a single-host, single-process deployment — exactly the in-scope use case for v1.x.
Observed evidence
everOS log near the hang
2026-06-28T12:33:41.329Z [info] document created doc_id=d_6e1ee6c085c1 topic_count=15
2026-06-28T12:33:41.334Z [info] POST /api/v1/knowledge/documents 201
2026-06-28T12:34:07.205Z [info] document created doc_id=d_845129d6c5d9 topic_count=1
2026-06-28T12:34:07.212Z [info] POST /api/v1/knowledge/documents 201
2026-06-28T12:37:20.458Z [info] GET /health 200 <-- last successful /health
2026-06-28T12:38:09.[...] [error] LLMError "Request timed out."
llama-server metrics during the hang
llamacpp:requests_processing 0 <-- LLM is idle
llamacpp:requests_deferred 0
llamacpp:n_busy_slots_per_decode 1.05
llamacpp:n_decode_total 22367
llamacpp:n_tokens_max 52811
The model has finished work. everOS is stuck after the LLM returns.
Python state of the hung everOS process
Open TCP connections: 1 ESTABLISHED from ingest client + ~5 internalThreads: 89 (vs ~50 at idle)Handles: 613Memory: ~450 MB RSS, slowly climbingCPU: ~3-5 % (only event-loop housekeeping)
A traceback captured at hang time:
File "...\starlette\middleware\errors.py", line 165, in __call__
await self.app(scope, receive, _send)
LLMError: 'Request timed out.'
httpx.ReadTimeout timeout=NOT_GIVEN
AsyncHTTP11Connection ['http://127.0.0.1:8585', CLOSED]
The connection to llama-server has been closed by the server side after some unknown timeout, but everOS's task is still awaiting it.
Reproduction shape
Single-machine reproduction on Windows + Python 3.12 + EverOS 1.1.0:
everos init, then edit<root>/everos.tomlwith any OpenAI-compatible LLM endpoint (we tested withQwen3.5-9B-UD-Q4_K_XLviallama-serverandminimax-m3viahttps://api.minimaxi.com/v1— both reproduce)everos server start- From another shell, upload 10–20 KB documents at ~10 s each via
POST /api/v1/knowledge/documentswithmultipart/form-data - Watch
kb-ingest.progress(orGET /api/v1/knowledge/documents) - After 2–5 uploads return 201, the next upload hangs
curl http://127.0.0.1:8000/health→ timeout- Killing
everosand restarting recovers, but the next round repeats
The Python client we used (urllib with timeout=300):
req = urllib.request.Request(url, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, method="POST")
with urllib.request.urlopen(req, timeout=300) as r:
return r.status, r.read()Expected behavior
Each POST /api/v1/knowledge/documents should:
- Accept the request
- Run
extract_knowledge(LLM call) and write the markdown + cascade entry - Return
201within bounded time
/health should keep returning 200 throughout, even while extract_atomic_facts / cascade are processing in the background. The cascade worker should not block user-facing API responses.
Suspected root cause
Based on observable behavior, the deadlock likely involves these pieces competing in the single event loop:
-
Runner.run()ininfra/ome/_dispatch/runner.py:128holds the OME engine semaphore for the entire retry chain (max_retries × timeout). When the inner LLM call exceeds the httpx client timeout, the retry keeps re-entering and the semaphore never releases. -
cascade_worker
_run_loop()callsextract_atomic_factsfor each newly-written doc. While processing one row, it holds an internal claim (status='processing'inmd_change_state) and is awaiting an LLM call. New requests that try to ingest a doc want the same LanceDB connection pool. -
LanceDBwrites (knowledge_topic.lance) serialize through a single writer. cascade_worker trying to upsert + the request handler trying to upsert +extract_atomic_factsall touching the same table → cross-task dependency on the same async resource. -
The semaphore is not a per-attempt timeout — if a downstream task (cascade) holds the slot and is itself awaiting the LLM, all upstream tasks wait indefinitely.
The combination of:
- single asyncio event loop
- one shared engine semaphore
- one cascade worker serializing through that semaphore
- one LanceDB writer per table
creates a deadlock window whenever LLM call duration exceeds request handler timeout and cascade worker happens to be processing the same table.
Workarounds we tried (all partial)
| Workaround | Effect |
|---|---|
Increase LLMConfig.timeout 60→300 s |
Less timeouts, but doesn't prevent hang — only LLM call returns faster, hang still happens downstream |
Disable cascade scanner via EVEROS_CASCADE_SCANNER_DISABLED=1 |
Reduces noise but cascade_worker still runs |
Reduce -c to give more llama-server headroom |
Doesn't help — bottleneck is not LLM |
| Add more llama-server slots | Doesn't help — only 1–2 active requests in flight at hang time |
Drop --reasoning on llama-server |
Helps indirectly but does not eliminate the hang |
The most reliable workaround we found is to bulk-import the markdown files directly into <root>/default_app/default_project/knowledge/Technology/... and then let cascade drain at its own pace with no concurrent ingest. But this defeats the use of the HTTP API and doesn't work for users who want to upload programmatically.
Environment
EverOS: 1.1.0 (PyPI) — also reproduced on 1.0.1
Python: 3.12
Runtime: bare-metal Windows 11
LLM: Qwen3.5-9B-UD-Q4_K_XL (local llama-server b9469, --reasoning off, --cache-type-k q8_0)
and minimax/minimax-m3 (https://api.minimaxi.com/v1)
Embedding: 9B via llama-server --embedding --pooling last, everOS truncates to 1024-d
KB size at hang: ~840 documents, ~5 GB LanceDB knowledge_topic
Concurrent uploads: 1 (sequential ingest script, ~10 s per file, ~80 KB chunks)
Possible fixes (suggestion only — maintainers decide)
This is not a small change, but a few directions that look promising:
- Make
Runner.run(and the OME semaphore) honor a per-attempt deadline so a stuck task can't hold the semaphore indefinitely. - Decouple cascade_worker from the OME engine semaphore — let it run in its own bounded queue with a smaller concurrency budget, so ingest requests never wait on cascade.
- Add a watchdog task that, every N seconds, force-releases stuck
processingrows and resets theirretry_count. - Make
/healthtruly independent — currently it goes through the same middleware chain that can be blocked.
Happy to test any patches or provide more traces if helpful.
Activity
gloryfromca commentedon Oct 14, 2026
Closing: not reproducible on 1.4.1, verified by running your scenario rather than waiting for a re-test. Two of the four mechanisms you proposed were real and have since been fixed — your root-cause #1 was correct.
Reproduction
EverOS 1.4.1 at `a345d08`, isolated memory root, real `everos server start`, OpenRouter `gpt-4.1-mini` + DeepInfra `Qwen3-Embedding-4B`/`Qwen3-Reranker-4B`. Twenty generated ~78 KB Markdown handbooks uploaded back-to-back through `POST /api/v2/knowledge/documents` (multipart), client timeout 300 s, with a 15 s `GET /health` probe between every upload — a hang is the symptom, so the probe timeout is deliberately short.
Two independent runs, 8 and 18 uploads, 26 uploads, zero health failures:
A separate probe issued while an upload was in flight returned in 4.9 ms. Your report had it hanging after 2–5 uploads; the second run passed that threshold three times over. (The run stopped at 18 of 20 because the harness's own lifetime cap killed my server — not an EverOS failure.)
What changed since 1.1.0
Your mechanism #1 was right, and it is fixed. `Runner.run` did hold the OME engine semaphore across the whole retry chain, backoff sleeps included — at 2026-06-29, one day after you filed, the code was literally `async with self._sem:` wrapping the entire attempt loop. It now takes the slot per attempt with the sleep outside, and the docstring spells out the failure mode you predicted: "Holding the slot across the sleep turned a partial outage into a total stall."
The half-open connection in your traceback is now bounded. `LLMSettings.timeout_seconds` (default 60 s) reaches both client constructors, so a task awaiting a connection the server already closed can no longer wait forever.
Mechanism #3 does not hold. LanceDB is driven through the async API (`connect_async`, `await table.query()...`), so its writes do not occupy the Python event loop.
Mechanism #4 is not disproven. `/health`'s cascade probe is wrapped so a locked SQLite cannot turn it into a 500, but that guards against an exception, not against a starved loop. It stayed responsive throughout, so it is no longer the most likely explanation — not that it is impossible.
If it still happens for you
The remaining plausible cause is memory pressure rather than deadlock, and two unmerged PRs target exactly that: #492 caps per-batch handler concurrency (50 concurrent handlers were measured driving a production replay to 8.29 GiB) and #491 stops
merge_insertrewriting large fragments for all-new batches. Both touchcascade/worker.py.If you can still reproduce on 1.4.1, please reopen with the EverOS version and RSS at the moment of the hang — that one number separates "deadlock" from "swap thrash" and we would chase it again. Thank you for the original diagnosis; the semaphore fix came out of it.