* fix(retrieval): serialize local embedding and reranking on MPS
On Apple Silicon the server process is killed outright (SIGSEGV or SIGTRAP, no traceback) partway through answering any question that retrieves from a knowledge base with hybrid search and a local reranking model. The client sees a dropped connection and the answer is lost.
Hybrid search fans its queries out concurrently and every task calls the same shared local model on a worker thread. Torch's Metal shader cache is a process-wide singleton whose lookup tables have no lock, so two of those threads racing inside it corrupt the cache and take the process down with it.
Guard the local SentenceTransformer and CrossEncoder calls with a shared lock that is only a real lock when the selected device is MPS. CPU and CUDA installs keep the concurrency they have today, and external reranking endpoints are untouched. Reranking several queries on a Mac now runs one at a time, which is the cost of the process staying alive.
Verified by driving the real hybrid-search fan-out with 16 concurrent queries: peak simultaneous entries into the local model drops from 16 to 1 on MPS, stays at 16 on CPU, and the returned documents, scores and ordering are byte-identical in every case.
Fixes#29722
* fix(evaluations): serialize the leaderboard embedder against retrieval on MPS
The leaderboard's tag-similarity search builds its own SentenceTransformer, and on Apple Silicon sentence-transformers places it on the MPS device. It runs on a worker thread, so an admin running a leaderboard search while anyone queries a knowledge base puts two threads into torch's Metal backend at the same time, which kills the server process outright with no traceback.
Move the lock added for the retrieval path into env.py, beside the device selection that decides whether MPS is used at all, and take it around the leaderboard's embedding calls as well. Sharing one lock between the two modules is the whole point, since two separate locks would still let a leaderboard search collide with a retrieval query.
Only inference is guarded, matching the retrieval path. Model construction stays as it is here and in the retrieval routers.
Verified by driving the leaderboard similarity path and retrieval reranking from six threads against one instrumented model: peak simultaneous entries drops from six to one on MPS, and the similarity scores are unchanged.
Related to #29722.
Uploading a file whose text contains literal HTML entities stored a rewritten copy of it: ` ` became a non-breaking space, `>` became `>`, and ` ` was decoded twice down to a bare non-breaking space. That stored text is what gets indexed and what the model reads, so notes, specs and source files reached the model differing from the file that was uploaded.
Every loaded document goes through `ftfy.fix_text`, which is there to repair mojibake left by the encoding-detection fallback. Its default configuration also decodes HTML entities, per line and sticky forward: entities are decoded on every line up to the first line holding a literal `<`, then left alone for the rest of the document. The same escape therefore survives or vanishes depending on where it sits in the file. This disables that one behaviour and leaves every other ftfy repair in place.
Text from a third-party extraction engine that returns escaped output now keeps those escapes. Guessing whether an escape is markup or content is the bug being fixed.
Fixes#29732
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
* ci: run the external regression suite on release pull requests
Adds a workflow that runs the open-webui/tests unit suite against release
candidates, so a release that reintroduces a fixed bug is caught before it is cut
rather than after users report it. The suite is roughly 4500 source-level tests
pinned to specific past issues and PRs, and takes about three minutes; the
dependency install dominates the run and is cached.
It runs only on pull requests into main whose title starts with a version, which
is how releases are titled here, or which touch package.json. Everything else
into main, and every pull request into dev, skips it and reports green.
Two settings are needed for this to block anything, both outside the diff:
require the Regression / Result check on main, and require branches to be up to
date before merging so the suite covers what actually lands.
The reusable workflow is referenced at @main so a release always runs the current
tests. Pinning it to a tag instead is a reasonable call to make here.
* ci: cancel superseded regression runs
A queued run on a release PR meant a stale commit's suite kept blocking
the required check after newer commits shipped, wasting a runner slot
and the author's time waiting on a result nobody needed. Cancel it
instead so the suite always runs against the latest push.
* ci: rename the Regression workflow to Tests
* Update regression.yaml
* ci: gate the test suite with a job condition instead of a gate job
Replaces the gate job with a condition on the suite job itself. The job existed
to look for a version title or a change to package.json, and the package.json
check is redundant: a release bumps the version in that file and carries it in
the title, so the title alone identifies one. That removes a runner, an API call
and the pull-requests read permission.
The suite now runs on version-titled pull requests from dev into main, and on
version-titled pull requests into dev so it can be exercised outside a release.
An edit only re-runs it when the title itself changed, and an edit no longer
cancels a suite that is already running, which would otherwise leave the check
green with nothing behind it.
* ci: match only the version prefixes releases actually use
Release pull requests are titled 0.11.3, not v0.11.3, so the leading v never
matched. The remaining digits are dropped with it and the dot is kept, so a
title that merely starts with a digit does not run the suite.
Signed-off-by: Adam Tao <tcx4c70@gmail.com>
Signed-off-by: Riley Des <riley.desserre@improving.com>
Signed-off-by: James Liounis <james.liounis@perplexity.ai>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com>
Co-authored-by: Teay <pythontogoplease@gmail.com>
Co-authored-by: tcx4c70 <tcx4c70@gmail.com>
Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com>
Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
Co-authored-by: RomualdYT <romuald@gameurnews.fr>
Co-authored-by: Lucas <lucas@vanosenbruggen.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: Constantine <Runixer@gmail.com>
Co-authored-by: Athanasios Oikonomou <athoik@gmail.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Aleix Dorca <aleixdorca@mac.com>
Co-authored-by: Vincent Agra <agravj007@gmail.com>
Co-authored-by: Shamil <ashm.tech@proton.me>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: looselyhuman <fieldian@gmail.com>
Co-authored-by: Circe (Claude Code Sonnet 4.6) <circe@athena-council.org>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: Daniel Nylander <po@danielnylander.se>
Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
Co-authored-by: Asbjørn Dyhrberg Thegler <asbjoern@dyhrbergthegler.dk>
Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com>
Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>
Co-authored-by: mayamsin <58122670+mayamsin@users.noreply.github.com>
Co-authored-by: G30 <50341825+silentoplayz@users.noreply.github.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: POV9en <POV9en@users.noreply.github.com>
Co-authored-by: oxsignal <oxsignal@users.noreply.github.com>
Co-authored-by: Dara Adib <dara@quietapple.org>
Co-authored-by: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com>
Co-authored-by: maco <gosarmarcel7@gmail.com>
Co-authored-by: Sakıp Han Dursun <100518315+sakiphan@users.noreply.github.com>
Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com>
Co-authored-by: Mateusz Hajder <6783135+mhajder@users.noreply.github.com>
Co-authored-by: Amir Subhi <amirsubhi@hotmail.com>
Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com>
Co-authored-by: bannert <58707896+bannert1337@users.noreply.github.com>
Co-authored-by: Boris Rybalkin <ribalkin@gmail.com>
Co-authored-by: HW <hwinkler@first-it-consulting.de>
Co-authored-by: sfwani <sfwani@users.noreply.github.com>
Co-authored-by: rileydes-improving <riley.desserre@improving.com>
Co-authored-by: Zixin Yu <183055163+ivvi0927@users.noreply.github.com>
Co-authored-by: Lukáš Kucharczyk <lukas@kucharczyk.xyz>
Co-authored-by: russelg <russelg@users.noreply.github.com>
Co-authored-by: Mr. Meowgi <ovehbe@gmail.com>
Co-authored-by: Zaid Marji <91486926+zaid-marji@users.noreply.github.com>
Co-authored-by: Craig <66838006+TipKnuckle@users.noreply.github.com>
Co-authored-by: James Liounis <james.liounis@perplexity.ai>
Co-authored-by: Chane Lu <2522992009@qq.com>
Co-authored-by: Justin Williams <justinjohnwilliams@gmail.com>
Co-authored-by: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com>
Co-authored-by: hungryBird <pantazis.marina@gmail.com>
Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch>
Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>
Co-authored-by: Sicknine <156204309+SNaytiP@users.noreply.github.com>
Co-authored-by: JuanMa Diaz <torgus@gmail.com>
Co-authored-by: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>
The colocated vitest files (shortcuts, the colon fence marked extension) now
live in open-webui/tests under frontend/, next to the rest of the regression
suite, where they run against the source of any ref through the shared
regression workflow. This removes the copies here; the unit-tests workflow job
and the test:frontend script stay and pass with no tests.
src/lib/utils/_template_old.ts goes as well: it imports vitest but its name
never matched the test glob, so those tests have not run since they were added.
Uploading an Arduino sketch (`.ino`) to a knowledge base failed with `Expecting value: line 1 column 1 (char 0)` whenever the content extraction engine was Tika or Docling. Browsers send `.ino` as `application/octet-stream`, and the extension was missing from the known source extension list, so the file was handed to the extraction server instead of being read as plain text. The server answered with a non-JSON body and the loader crashed while decoding it. `.cpp` and `.h` sketches in the same folder uploaded fine, because those extensions are already on the list.
Adding `ino` to that list routes it to the plain text loader, the same way the yaml/toml gap was closed in 710320601a. A sketch is plain C++ text, so there is nothing for a document extraction server to do with it.
Verified by dispatch matrix over 35 extensions, 5 content types and all 8 engines against a stub server that reproduces the non-JSON response: the only rows that change are `.ino` under Tika and Docling, which now resolve to the text loader and extract the sketch verbatim. Every other row is unchanged.
Fixes#29670
A response with exactly one citation never showed its relevance badge, even though the source carried a perfectly valid score. Adding a second citation made the badge appear for that same source, so the score looked like it came and went at random.
calculateShowRelevance hides relevance when a result set mixes distance metrics (cosine in -1..1 next to unbounded L2), since those numbers are not comparable. With a single distance that check degenerates: distances.length - 1 is 0, so the "all but one are out of range" clause matches whatever the value is, and the score is hidden. A lone distance cannot be a mix of two metrics, so it now returns before the outlier check runs.
Sets of two or more distances behave exactly as before, mixed-metric sets are still suppressed.
Fixes#29646
Sending a message to an arena model failed with "'JSONResponse' object has no attribute 'body_iterator'" whenever the backing provider answered with an HTTP error, so the real error never reached the user. Non-streaming requests on an arena model, such as title and tag generation, broke the same way with "'JSONResponse' object is not a mapping".
The arena wrapper assumed the sub-model call always returns a stream for a streaming request and a dict for everything else, but the OpenAI-compatible router returns a plain response object as soon as the provider answers 4xx or 5xx. Both arms now hand that response straight back, which is exactly what the non-arena path already does, so the existing error handling turns it into the usual error message in the chat.
Verified against a matrix of streaming and non-streaming requests with the sub-model returning a stream, a dict, a JSONResponse and a PlainTextResponse: both crashes are gone and the two success paths are unchanged, including the selected_model_id prelude on the stream.
Fixes#29658
Adopts the loanwords normally used in Spanish-language developer and AI
interfaces for three terms the locale currently calques, and applies each
one consistently across the file.
- prompt: Indicador -> Prompt (74 strings)
- pipeline: Tubería -> Pipeline (14 strings)
- delete: Borrar -> Eliminar (62 strings), matching the rendering the
locale already uses for "remove" and closing the Borrar /
Eliminar split that applied both words to the same action
- Admin -> Administrador, Email -> Correo electrónico (3 strings)
"Indicador" means indicator or gauge and is not how Spanish-language AI
tools refer to a prompt. "Tubería" is a physical pipe; the file already
hedged once with "Tuberías (Pipelines)". Gender agreement is updated where
it changes, since Pipeline is masculine and Tubería feminine.
Before this change the locale rendered "delete" as Borrar in 34 strings and
Eliminar in 28, with no distinction in meaning between them.
This changes wording contributed by the Spanish community rather than
fixing defects, so it is a terminology decision rather than a correctness
fix, and is kept separate from the translation work for that reason.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two entries lost their interpolation placeholder, so they render
literally instead of being substituted:
- "{{ models }}" had the placeholder name itself translated
- the 50-word summary prompt replaced {{topic}} with a bracketed note
The rest change the meaning or contradict neighbouring strings:
- "Enter Key Behavior" was read as the verb "enter", turning the label
for how the Enter key behaves into "type in the behaviour key", while
its own description already explains the setting correctly
- "Regenerate Menu" was read as "refresh the menu" rather than the menu
of regenerate actions its description describes
- "Enter reasoning effort" was not meaningful Russian
- "Enter Bing Search V7 Subscription Key" mentioned an endpoint that the
source string does not
- "Web Search in Chat" called the feature a search engine
- "Enter Application DN" said ND; "Enter Bocha Search API Key" said APi
- "Toast Notifications for New Updates" dropped "new" and used the wrong
form of the preposition
- "Enter Top K Reranker" used a term its own label does not, and
"Select a reranking model engine" differed from every other reranking
string
- two of the four iframe sandbox switches did not follow the wording of
the other two
- "Follow-Up Auto-Generation" capitalised a word mid-phrase
Only values changed; the keys and their order are untouched.
Chunk metadata inserted into the vector DB carries arbitrary client-
supplied fields (e.g. custom metadata from file uploads), so it needs
the same size cap, type coercion and null-byte sanitization every
other backend already applies through process_metadata(). Four
backends never called it: Qdrant, Qdrant multitenancy, Milvus
multitenancy and Oracle23ai, so an oversized or malformed metadata
blob went into those unfiltered.
Wire process_metadata() in at each backend's single insert/upsert
chokepoint, matching the pattern already used by the other eleven
backends.
The built-in chat and knowledge grep tools awaited their matching helper directly, so the search ran on the event loop and held it for as long as the match took. Both call sites now hand the helper to a worker thread.
Output and error handling are unchanged: 17 cases (literal, regex, case-insensitive, count-only, no-match, invalid pattern, rejected quantifiers, missing file data, result truncation) compare byte-for-byte against the previous behaviour. The matcher's time budget lives in a contextvar, which asyncio.to_thread copies into the worker, so budget scoping still behaves as before.
Inter ships arrow substitutions as OpenType contextual alternates, and
Open WebUI sets no font-variant-ligatures, so a sequence like <--- is
stored and sent exactly as typed while being drawn as an arrow. Reported
as text conversion in #29594, where the memory rule had in fact worked
and only the display was misleading.
Reading the DOM back with the bundled Inter file loaded in isolation
confirms every field holds U+3C U+2D U+2D U+2D. Only the glyphs differ.
no-contextual rather than none: none, no-contextual, and
font-feature-settings "calt" 0 all fix it, which places the arrow in
contextual alternates. no-contextual disables that one feature and
leaves standard ligatures intact, so ordinary prose is unaffected.
Set on html, next to the existing document-wide typography, so it
inherits to editable fields and rendered content without a new selector.
Scoping it to inputs would fix the field you type into and leave the
memory list and chat messages still drawing arrows.
GET /api/version/updates returned the running version as latest whenever
the GitHub request failed, so an instance that cannot reach GitHub
reported itself up to date however far behind it was. The exception was
logged at debug, below the default level, so nothing recorded that the
check never happened.
The failure path now returns latest: None and logs at warning.
A null latest cannot be passed to compareVersion as it stood.
current.localeCompare(null) coerces to the string "null", and "0.10.2"
sorts before it, so the function returned true. The backend change alone
would have turned a false (latest) into a false update-available plus a
toast, so the guard is part of the fix.
The three callers stop substituting the running version in their catch,
and the two badge surfaces gain a third state. When latest is unknown
the badge is plain text, since there is no release to link to.
Admin Settings > General was wrong in a worse way than reported: it
initialised updateAvailable false with latest set to the running
version, and never checked on mount, so it claimed (latest) having made
no request at all. It now matches About.svelte, which starts unknown and
checks on mount.
Closes#29580
A web search is not something that can be cited. The search engine
returns a title, a link and a one line snippet for each hit, and the
model never opens any of those pages. Emitting them as citation sources
produced one <source> tag per result, all named search_web with empty
bodies, and the citation template then instructed the model to cite them
by id. Models either hesitated visibly or attached an id to content from
a different result, which the citations panel then resolved to a title
that looked plausible, so the misattribution read as correct.
Web search results now stay in the tool output the model reads, and stop
being offered as things to cite. Where the model needs to cite a page it
calls fetch_url, whose citation names the URL and already works.
Web search results no longer appear in the citations panel. That is the
point of the change: the panel was offering pages that nothing had read.
Scoped to the native tool-calling path. The legacy handler cites every
tool result as one opaque source and does not single out web search, so
it is left alone rather than special-cased.
Saving any tool or function in the admin UI threw "ModuleNotFoundError:
No module named 'click'" in the browser, even on a fresh tool with no
custom code, because the click name never appears in user code at all.
The in-browser formatter runs black through a Pyodide/micropip worker.
black needs click, mypy_extensions, pathspec, platformdirs and pytokens
at import time, but the vendored pyodide-lock.json records none of
these as black's dependencies, and micropip only walks a locked
package's declared deps when install() is given explicit constraints,
which this worker never does. So only black itself ever got installed.
Requesting the five packages explicitly alongside black fixes it
without touching the worker or the lock file.
Ten strings were still untranslated in de-DE, so German users saw raw English keys in the settings sidebar and in the interface settings. The settings group headings (Basics, Services, Preferences, Data, AI, Quality, Experience) all rendered in English above otherwise German tab names, plus the font family label and its description, and the multi-file upload failure toast in a knowledge base.
All ten now have German values, chosen to match the terminology the catalog already uses: AI as KI, Services as Dienste (matching Dienstkonto and the existing service endpoint strings), Interface as Benutzeroberfläche in the font description, and the third-person descriptive voice the neighbouring setting descriptions use. Experience is rendered as Benutzererlebnis rather than Darstellung because that group covers audio as well as interface and images. Both interpolation placeholders in the upload toast are preserved.
Only de-DE is touched. No key was added, removed or reordered, and no already-translated string was changed. The catalog now has no empty values left.
Fills the 917 empty strings in the es-ES locale and repairs 94 existing
entries that rendered incorrectly. No keys are added or removed, and the
terminology chosen by the Spanish community is preserved throughout: new
strings reuse the glossary already present in the file.
Filled strings:
- Reuse existing renderings so each English term keeps one Spanish form
(chunk -> Fragmento, knowledge base -> Base de Conocimiento,
workspace -> Espacio de Trabajo, skill -> Habilidad).
- Include the settings sidebar group headers added in 006f95ee5
(Basics, Services, Preferences, Data, AI, Quality, Experience), which
were otherwise falling back to English in the settings modal.
- Add real grammatical plural forms for every one/many/other variant.
Spanish requires the `many` CLDR category, which English does not have.
- Preserve every {{placeholder}}, backtick, URL, HTML tag and CLI flag.
Repaired strings:
- 4 broken interpolations, including "Deleted {{name}}", whose placeholder
had itself been translated to {{nombre}} and so rendered literally.
- 6 plural groups whose values carried the i18next key suffix as visible
text (e.g. "{{count}} seleccionados_únicos"), and one where "_one" had
been translated as the adjective "únicas".
- "Reset" now reads Restablecer rather than Reiniciar ("restart"), which
misdescribed destructive actions such as Reset Vector Storage/Knowledge.
- "Access" (Acceso) separated from "Permissions" (Permisos); the *Access
family was previously split between the two.
- "Chats Public Sharing" and "Chats Open Sharing" no longer render
identically in the group permissions panel.
- "You" corrected to "Tú"; unaccented "Tu" is the possessive "your", and
this string labels every user message bubble.
- "Channels" corrected from the singular "Canal".
- ~49 misspellings and missing accents (Conexxión, Interprete, Publicamente,
Busqueda, Incrustración, Añador, Wev, actualiada, mantentrá).
Verified: 0 empty strings, key set identical to dev, 0 placeholder
mismatches, prettier --check passes, and all strings render correctly
through i18next 23.16.8 using the application's own options.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The settings navigation group labels added in 006f95ee5 (Basics,
Services, Preferences, Data, Experience, AI, Quality) were left
empty in every locale, so ru-RU and uk-UA fell back to the English
key text. Fill them for both locales.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Fill all remaining untranslated strings so both locales reach full
coverage:
- ru-RU: 967 strings translated
- uk-UA: 2110 strings translated
ru-RU also aligns terminology with the existing translations
(webhook -> вебхук, sub-agent -> субагент, reranker -> реранкер,
chunk -> чанк, parsing -> розбір/разбор) and standardizes "web search"
to «Поисковая система» across the whole file.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Custom metadata attached to a file upload (e.g. via the API's
metadata field) was stored on the file row but silently dropped
when the file was chunked and embedded for RAG, so it never reached
the LLM through retrieved sources.
process_file() builds chunk metadata in several branches; three of
them already merge the file's meta dict in, but the branch used for
a fresh upload processed through a document loader did not. Bring
it in line with the others so custom metadata flows through
consistently regardless of upload path.
Reported in open-webui/open-webui#29486.
A direct-connection streaming request registers a per-request socket.io handler before it asks the browser to start the completion, and that handler was only removed once the response had been fully streamed. Every other way the request could end left it behind for the life of the process: the call to the browser raising, a non-success status, an ack with no arguments or an ack that is not an object, cancellation while waiting for that ack and a response body closed or garbage-collected before it finished. A client that repeatedly hits a failing direct connection grows the server's handler table and the closures it holds without bound, and nothing ever cleans it up.
The removal now runs on every exit from the request, through one named helper that pops with a default so it is safe to run twice on the paths where both the generator's finally and the response's background task fire. The guard covers the exchange up to the status read and catches BaseException, because cancellation is not an Exception, and re-raises it unchanged.
Measured across thirteen exit paths: twelve leak a handler on dev and none of those leak here. The thirteenth, a response body that is never iterated at all, behaves the same on both. 200 failing requests leave 200 handlers on dev and none on this branch. Streamed bytes on the success path, exception types and cancellation behaviour are unchanged.
The regex detection helpers wrote their backslash escapes in ordinary string
literals, so Python reported six invalid escape sequences on import. They work
today because Python leaves an unrecognised escape as its two characters, but
that behaviour is deprecated and becomes a syntax error in a future release, at
which point the knowledge filesystem tools stop importing at all.
The literals are now raw, which is the same two characters with no warning.
Pattern detection and normalisation are unchanged: verified identical output
over every string up to length five drawn from the characters these helpers
look at.
The `/` command menu is portalled into a tippy popup by
getSuggestionRenderer, and tippy's base stylesheet sets
.tippy-box { color: #fff }. The app's `transparent` theme neutralises
tippy's background, padding and margin but never its text colour, so
everything inside the popup inherits white.
DropdownMenu paints bg-white dark:bg-gray-850 dark:text-white, defining
a text colour only for dark mode, and the command rows in SlashCommands
set no colour of their own. In light theme that leaves Temporary, Model
and Settings as white text on a white menu.
Reset the colour on the transparent theme so the popup inherits from
body, which is #000 in light and #eee in dark. The secondary text and
the prompt rows were already unaffected because they set explicit
theme-aware colours.
Keeps the entries already on dev and merges in the two this branch carried, then
puts the section back into the shape the format asks for.
Added now holds the contrast accessibility mode gives the dropdown menus, their
submenus and the model picker, followed by the general improvements placeholder
and the translation entry in their reserved positions. The translation entry
moves out of Changed, takes the standard wording and drops its link, which that
entry never carries; Changed is left out, having nothing else in it.
Each entry is condensed to a single sentence, the backticked column name becomes
a quoted one, and the interface font entry gains the commit it came from, having
carried no reference at all. Fixed is ordered by reach, from the conversation
branches and the failed upgrade down to the font and the disconnect control.