v0.4.2 adds nothing. No new features, no breaking API changes, no schema migrations — 61 commits of fixes on top of v0.4.1. If you self-host ChaosCypher, three of them matter enough to upgrade for: live chat streaming works again, a queued task could create database files wherever it liked, and the API stopped shipping whole payloads on every poll.
The bug that hid behind a working feature
Live chat streaming was dead, and the product looked fine anyway.
The middleware that disconnects request-scoped storage adapters ran its teardown as soon as the response object existed — not when the response had finished sending. On GET /chats/{id}/events the generator's first suspension is the pub/sub subscribe, and the reconcile right after it hit an adapter that had just been disconnected. So every live stream died with STREAM_INTERNAL_ERROR before relaying a single token, whenever the chat was actually processing.
What made it survive review is what happened next: the answer still appeared when you reloaded. The worker had run, the turn was persisted, the data was correct. Only the live token-by-token experience was gone — which reads as "streaming feels broken today" rather than "a middleware is tearing down storage under an open response".
The middleware is now pure ASGI and tears down after the whole response is sent. That fixes the class, not just the symptom: any streaming endpoint or background task that touches an adapter after the response object is produced was exposed to the same teardown. It previously had no tests at all; it now has seven, two of which fail against the old implementation.
In plain English: chat answers stream live again, and the piece of plumbing that broke them is now covered by tests.
Three security fixes worth reading if you run this on a LAN
A queued task could create SQLite files at a path of its choosing. metadata.database_name on POST /api/v1/queue/tasks reached a bare Path join in get_db_path, which then mkdir'd and created a database file wherever the value pointed. The sink now enforces the same [A-Za-z0-9_-]+ fullmatch that BackupService and the queue handlers already applied — placed at the sink, so it covers every reader of that metadata field at once rather than one caller at a time.
The health endpoint was an unthrottled bcrypt oracle. location = /api/v1/health was the one auth_request location in the nginx templates without a limit_req, so an unauthenticated host on your LAN could drive one bcrypt hash (cost 12) per stored API key, per request. It now carries the same rate limit as its siblings.
Per-IP auth rate limiting now enforces the policy it declares. This one was two broken halves of a single control. proxy-public.conf blanked X-Auth-Edge-Token on exactly the public auth routes, which collapsed the app layer's per-IP login and setup buckets into one global bucket keyed to the nginx loopback peer — so any host on your network could starve your own login. Separately, the nginx auth zone rendered login_max_requests as requests per second, 60× the configured per-window policy, while every *_window_seconds setting was silently ignored and setup_max_requests never rendered at all, because both locations shared one zone. Zones now render as floored requests-per-minute, /setup gets its own zone, and burst scales with the per-window count.
Worth stating plainly, because the changelog does: the rate-limiting defect was an availability problem, not an auth bypass — X-Auth-User was never trusted from outside the edge.
Riding along: MCP read mode no longer leaks five write tools that were missing from TOOL_DEFINITIONS and fell through to bridge.execute; extract_archive() enforces the member-count, declared-total, and streamed-byte caps at the function boundary where untrusted archives actually land; credentials-file updates take a cross-process lock, so with uvicorn_workers > 1 a logout can no longer be clobbered back into validity; and a settings PATCH of allowed_origins: ["*"] with allow_credentials: true is rejected with a 422 instead of persisting and then making every subsequent boot SystemExit with no API path back in.
In plain English: a few things that were reachable from your local network, and one setting that could brick a restart, are closed.
Work that reported success while disappearing
The data-correctness fixes in this release share a property: each one logged or displayed success while losing, hiding, or overwriting something.
A task failing before handler dispatch was silently lost. The queue worker's outer try had no except, the done-callback never retrieved the task exception, and the finally removed the task from the running set while its hash still read queued. The task then existed in neither pending nor running — invisible to the reconciler and to rehydration. It never ran and never reported. Such a task is now marked failed-terminal (visible, dead-lettered) and the poller logs the exception through the canonical path.
A search-index sweep could clobber a permanently failed source back to indexed. The exhaustion branch deletes its queue row before marking the source failed, so a sibling draining later in the same batch saw zero survivors and flipped the source to indexed — hiding a source that would never be searchable. Both indexed-flip sites now refuse to overwrite a terminal failed status.
Confirming a source with a forced domain could extract under the wrong one. forced_domain and other non-None overrides were written in a second transaction after the atomic claim, while gate_decision short-circuits on the claim timestamp alone. An import analysis snapshotting inside that window saw a confirmed-but-domainless source and extracted with the auto-detected domain instead of the one you chose. Everything now rides the single write-once claim.
Alongside those: plain chat send was the only turn-enqueue path still missing the double-enqueue guard; the stuck-chat sweeper could stamp an error over an answer that had just completed; a pause landing mid-health-tick was re-attributed to health_monitor and later auto-lifted; vision page retry counters could drift permanently into completed + failed < total_pages; reset_all() could leave the app pointed at a schema that no longer existed; resetting the knowledge base could leave you with zero templates; and the migration lock file is no longer unlinked while a waiter holds it, which had let a third process lock a fresh inode and run backup-and-upgrade concurrently with an in-flight upgrade.
In plain English: several paths used to say "done" while quietly dropping or overwriting your work. They don't any more.
The API got a lot lighter per poll
None of this is a benchmark — it is a description of what the code was doing, from the changelog:
-
Every search ran a full 57-column source listing at page size 100,000.
_get_enabled_source_ids()now uses a single-column projection through a new storage-protocol accessor, which also removes the >999-source SQLite parameter-limit hazard the old path could hit. -
GET /queue/tasksshipped whole task payloads — including LLMmessagesarrays — on two five-second polls. The list endpoint now returns a whitelisted subset (inputs.filename,inputs.analysis_depth,operations_count). If you consume the list endpoint'sdatablob from your own tooling, this is the one change in this release you may need to adjust for. The detail endpoint is unchanged and still returns the full payload. -
get_sourceread and twice-copied the entire raw upload on a three-second poll.full_textnow joins the heavy-column set the response model was discarding anyway, with a narrow accessor kept for the CCX export path that genuinely needs it. -
Chunk hydration batches instead of looping. Four chat and workflow tool handlers replaced per-hit
get_chunk_by_idloops of up to ~100 queries per turn with a single batch fetch each, and the batch projection excludes the ~5 KB-per-hit embedding and the raw content. - Search hydration stops fetching embeddings it never reads — and a follow-up applied the same projection to twelve more batch-fetch paths, none of which read the 1024-float embedding they were hydrating.
In plain English: the screens that poll — queue, source detail, search — now ask the database for the columns they actually display.
If you run the multi-container stack
The multi-container login screen and setup wizard were broken: multi-interface-nginx.conf had drifted from its template and was missing the auth-exempt settings/public and settings/host locations, so the SPA got 401s before login. The static config is re-synced.
Also in that stack: the Valkey wipe sentinel is now written where the worker actually reads it (/data/.valkey_was_wiped, not /run/chaoscypher/...), so forced queue rehydration can fire after an AOF wipe; the boot splash ships its eight security headers, which nginx had been dropping through inherited add_header; and the production compose file no longer points LEXICON_URL at a dev-only host.
In plain English: if you run the multi-container stack rather than the all-in-one image, this release repairs login, queue recovery after a Valkey wipe, and a few config defaults.
Upgrading
No migrations apply — this release has no schema changes at all.
Drain the queue before swapping the image: stop new submissions and wait for /api/v1/queue/stats to report 0 pending on all queues. Payload-version negotiation still isn't implemented, so don't run mixed versions against the same queue.
docker pull ghcr.io/chaoscypherinc/chaoscypher:0.4.2
Or, if you run the Python packages directly:
pip install -U chaoscypher-core chaoscypher-cortex chaoscypher-neuron chaoscypher-cli
Starting fresh:
docker run -d --name chaoscypher \
-p 80:80 \
-p 443:443 \
-v chaoscypher-data:/data \
--add-host=host.docker.internal:host-gateway \
ghcr.io/chaoscypherinc/chaoscypher:latest
Next steps
- Upgrade, then reopen a chat and watch an answer stream in — that is the fix you will notice first.
- If you read
GET /api/v1/queue/tasksfrom your own tooling, check whether you depended on fields outsideinputs.filename,inputs.analysis_depth, andoperations_count; the detail endpoint still carries the full payload.
Full details in the changelog. ChaosCypher is AGPL-3.0 and local-first — the graph, the chat, the import and export paths all run on your own machine. Repo: https://github.com/chaoscypherinc/chaoscypher


Top comments (0)