# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.14.2] - 2026-09-22 **Released:** `@localmode/chrome-ai` 2.2.1, `@localmode/bench` 0.8.2. ### Fixed - **chrome-ai:** a response Chrome cuts at its output cap ("The response exceeded output limits and was truncated.", a `QuotaExceededError` on Chrome 153) was reported as an oversized input and, in `doStream()`, the text already streamed was discarded. `doStream()` now keeps that text and ends with `finishReason: 'length'`; `doGenerate()` fails with the new code `chrome-ai-output-truncated`. Found by the bench's Gemini Nano long-context cell failing intermittently on two Apple laptops after Chrome 153; from now on that cell records a truncated answer instead of an error. - **bench (analysis tooling):** `scripts/analyze.ts` analysed only runs of the current protocol (it validated each file as a submission). `validateSubmission(run, { anyProtocol: true })` accepts every archived protocol identifier; the CLI uses it and now covers the whole dataset, each row labelled by `protocol`. ## [2.14.1] - 2026-09-22 **Released:** `@localmode/bench` 0.8.1 (docs only). ### Added - **bench:** the leaderboard table is paginated: 25 rows a page (25 / 50 / 100 selectable), Previous / Next buttons with accessible names, a live "Showing x to y of n rows · page p of q" status, and a return to page 1 whenever a filter changes (a page past the end after a filter shrinks the list clamps to the last page). The arithmetic is a pure helper (`src/lib/bench/paginate.ts`) with unit tests; the table now holds 672 rows across two protocols, which was one long scroll. ### Changed - **bench (docs):** the v5 wording on the 2,048-token context overstated its effect. The first v5 Thorough run on the 16 GB M4 shows the llama.cpp CPU lane's Gemma 4 E2B chat cells running as before while its quality cell still fails on the per-request state allocation (`std::bad_alloc`) with the 3.46 GB weights resident in the 4 GB wasm heap; it is recorded as an error exactly as under v4. The methodology page's v5 entry and the bench README now say so. No code change; the protocol is unchanged. ## [2.14.0] - 2026-09-22 Protocol `localmode-bench/5`: the llama.cpp lanes' thread count and context size, and a leaderboard that shows v4 beside v5. **Released:** `@localmode/bench` 0.8.0. ### Changed - **bench (protocol v5):** the two llama.cpp lanes request half the browser's logical thread count (at least two) instead of all of it, and load with a 2,048-token context (embedding lane: 512). Under v4 the CPU lane asked for every logical thread; on hybrid and SMT processors that pool runs at half speed with high variance (native M1 Pro sweep: tg128 178 ± 42 tokens/s at 10 threads, 395 ± 16 at 8; browser: the M4 Max's 16-thread pool decoded a third as fast as the M1 Pro's 10). The browser exposes no core topology, so half the logical count is the rule; it lands on the performance or physical cores on every lab device. The smaller context keeps the Gemma 4 E2B GGUF's KV cache inside the CPU lane's 4 GB wasm heap, where the 8,192 default failed the quality cell on every v4 run. `n_ctx` joins `n_threads`, `multithread` and `n_threads_used` on each cell's `runtimeConfig`. Every other lane measures exactly as under v4. Harness 0.8.0. - **bench (leaderboard):** rows carry their protocol version and never mix versions; the leaderboard shows the current protocol and the previous one side by side (`LEADERBOARD_PROTOCOL_VERSIONS`), with a Protocol column and filter, so the v4 archive stays visible next to v5 instead of disappearing on the bump. The API's `runs` count and the page caption follow the same rule; the methodology page documents it and carries the v5 changelog entry. ## [2.13.3] - 2026-09-22 **Released:** `@localmode/wllama` 3.4.2. ### Added - **wllama, bench:** the llama.cpp lanes record the thread pool wllama actually built. `WllamaLanguageModel` and `WllamaEmbeddingModel` expose `threadPool` (`{ multithread, threads }` from the runtime's own `isMultithread()` / `getNumThreads()`), and the bench adapters write it into each wllama cell's `runtimeConfig` as `multithread` and `n_threads_used` beside the requested `n_threads`. Firefox 156 ran both llama.cpp lanes at single-thread speed with 10 threads requested and the run files could not say why; from now on a pool that fell back to one thread is recorded as such. Metadata only, no measurement changes; the protocol version is unchanged. ## [2.13.2] - 2026-09-22 **Released:** `@localmode/wllama` 3.4.1. ### Fixed - **wllama, bench:** the declared sizes of five GGUFs were wrong in both the provider catalog and the bench pairing catalog, found when the native baseline downloaded the same files: SmolLM2 135M Q4_K_M is 105 MB (declared 70), Qwen3 0.6B Q4_K_M is 397 MB (declared 530), Llama 3.2 1B Q4_K_M 808 MB (declared 750), Gemma 4 E2B Q4_K_M 3.46 GB decimal (declared 3.46 GiB), bge-small-en-v1.5 Q8_0 36.8 MB (declared 35 MiB). The runner's download estimate and the docs tables now say the real sizes. The declared size also selects a cell's decode-rate envelope class at validation: Qwen3 moves from the "under 1 GB" class to the "under 500 MB" class (a looser bound, 4,000 chars/s instead of 2,500; the highest Qwen3 decode ever recorded is 550). Archived runs carry their own declared sizes and are validated as they were; the envelope table itself is unchanged, so the protocol version is not. ## [2.13.1] - 2026-09-22 **Released:** `@localmode/bench` 0.7.1. ### Added - **bench:** device subclass on the leaderboard. Rows grouped by the coarse device class (platform + WebGPU vendor-architecture) put every Apple Silicon generation in one `macos/apple-metal-3` row. `refineDeviceClass()` splits a class by the GPU model where the browser names a specific part (`macos/apple-m1-pro`, `macos/apple-m4-max`, `android/adreno-650`) and leaves it alone where the browser names nothing more specific (WebKit's `Apple GPU`, the generation-less `AMD Radeon(TM) Graphics`, Firefox's masked buckets) or where there is no WebGPU. The leaderboard and the recent-submissions table show the subclass with the coarse class beneath it; index entries written from now on carry `deviceSubclass`, and older entries derive it from their `gpuModel` at aggregation time, so the archive is not rewritten. CSV exports gain a `deviceSubclass` column. ### Changed - **bench:** the methodology page now states the two device groupings (coarse class and GPU-model subclass, with what each browser can supply), the frozen iOS 18_7 token, Chrome's capped memory buckets, the single-use nonce and the 20-per-hour submission limit, and that a runtime-raised abort is an error with a retry while only the submitter's Cancel ends a run. ### Fixed - **bench:** iOS runs from Safari 27 reported `os.version: "18.7"`. WebKit froze the OS token in the UA at `18_7`, so an iPhone on iOS 27 reads as iOS 18.7 in Safari and in Firefox for iOS, while Chrome for iOS on the same phone writes the real version. The capture marks the frozen pairing `unknown-frozen` (a Safari with a `Version/18.x` token keeps its real 18.x); the Safari version stays the signal for the OS generation. Harness 0.7.1. Archived runs are unchanged. ## [2.13.0] - 2026-09-21 Privacy pass on the published run files, from a review of what a public run carries. **Released:** `@localmode/bench` 0.7.0 (result schema 3; protocol unchanged at `localmode-bench/4`). ### Changed - **A public run file no longer carries anything that locates or tracks a person.** The capture dropped the time zone, UTC offset and calendar (a city-sized location), the language list, the battery's exact percentage and time-to-full (a device-tracking signal; the level is kept to the quarter, plus whether it is charging), and display preferences; the submission nonce is stripped by the server and the digest no longer covers it, so a published file verifies as submitted; the submitter's network address was never written and the README now says so. `scrubRunForPublication()` is the one rule, applied on every submission (a payload from a page built before schema 3 is cleaned, its digest recomputed and `scrubbedAt` stamped) and by the dataset repository's `tools/scrub-publication.mjs`, which rewrote every file published earlier on 2026-09-21. The methodology page states what a public file does not carry. - **A session nonce fronts one submission.** Nonces are valid for six hours and were reusable, so a nonce copied from a fresh public run could front fabricated submissions until it expired; the server now consumes each nonce once (Redis `SET NX EX` when bound, in-instance set otherwise) and answers 409 `nonce-used` on a repeat. Route-level tests drive the real handler with only GitHub stubbed: clean submission published minus the nonce with the client digest intact, pre-schema-3 payload scrubbed with a recomputed digest, nonce refused on second use, wrong digest refused before anything is stored. - **Phone guidance on the runner page names the memory precondition.** The first crash record from an iPhone under 2.12.2 showed the pattern: the page started short on memory (the Transformers.js WASM worker could not get a heap for a 35 MB model, "[wasm] RangeError: Out of memory"), and the browser killed the page twelve seconds later during WebLLM's first inference; the same phone had completed four Quick runs earlier in the day. The note under the suite picker now tells phone users to close other tabs and apps first, and that a run whose first model fails with "out of memory" rarely survives the next one. ## [2.12.2] - 2026-09-21 **Released:** `@localmode/bench` 0.6.1. ### Fixed - **"Too many submissions" during a multi-device batch.** The submit endpoint allowed 5 submissions per hour per client address, and every device in a household (or an office, or a campus) shares one address, so the sixth run of a lab batch was refused. The limit is now 20 per hour per address (`SUBMIT_RATE_LIMIT`; the nonce, digest, shape, and plausibility checks are what keep the dataset honest, this only bounds volume), a refusal carries `Retry-After` and `retryAfterSec` with a message that names the wait, and the runner keeps the run and resubmits it by itself when the window opens, with a live countdown ("Retrying automatically in 12 m 30 s"); "Retry submission" and Export JSON stay available meanwhile. - **"Cancel run" reacts on the first click** (`@localmode/bench` 0.6.1). The runner's watchdog raced each unit of work only against its own timeout, so a cancel during work that ignores its abort signal (a model download in flight, a generation inside a runtime with no stop) waited for that work to finish: several seconds and repeated clicks on a download. The parent abort now rejects the watchdog immediately, the abandoned work's late outcome is discarded quietly, and a model that finishes loading after its cell was cancelled is released instead of leaking a runtime. The button switches to "Stopping…" on the click itself. Measured in real Chrome on the dev build: overlay gone 203 ms after a click during a model load and 98 ms during a WebLLM generation, no unhandled rejections. - **The run overlay never extends past the viewport and scrolls inside itself.** On windows at least 640 px wide the overlay centered the dialog in a flex box, so a dialog taller than the window was cut off at the top (unreachable by scrolling) and at the bottom, and the page behind it kept its own scrollbar. The dialog is now capped at the viewport minus its margins and scrolls internally, and the page scroll is locked while the overlay is open, so there is one scrollbar. Measured in real Chrome at 1100x560: dialog from 24 px to 536 px, 708 px of content scrolling in a 510 px box, page scroll locked and restored on close. Taking focus on open no longer scrolls the top margin away. - **`pnpm run dev` and the new `pnpm run dev:bench` watch the workspace packages.** The root `dev` ran the packages' `tsup --watch` sequentially, so the first watcher blocked everything after it and an edit to `@localmode/bench` or `@localmode/wllama` reached the app only after a manual build; `dev` is now parallel, and `dev:bench` runs the two package watchers with the ui dev server. - **The overlay's retry notices are one plain sentence each.** A retry showed the raw error with its stack fragment (a Windows run listed five lines of "Failed to execute 'requestDevice' on 'GPUAdapter': D3D12 create command queue failed with DXGI_ERROR_DEVICE_REMOVED (0x887A0005) at CheckHRESULTImpl (..\\..\\third_party\\dawn\\...)"). The overlay now says " · : the GPU device was lost; trying again (attempt 2)", with the memory, download, and stall cases named the same way, shows the last three, and leaves the full error, cause, and stack on the cell's `attempts` in the run record. - **Crash diagnostics carry the page's memory at the last phase change.** Where the browser exposes it (Chromium), the saved attempt records the page memory sampled when the running lane's phase began, and Copy diagnostics prints it ("page memory when that phase began 1.95 GB"), so a page killed during a model load on an 8 GB machine can be told apart from a GPU crash. - **A runtime's own `AbortError` no longer ends the run as "Cancelled"** (`@localmode/bench` 0.6.1). The runner and the page equated any `AbortError` with the Cancel button, so when a browser dropped a model download mid-suite (an Android phone did this on every attempt) the overlay closed with "Cancelled" and every finished cell was discarded. A cancel is now recognised only from the page's own abort signal; a runtime-raised AbortError is a cell error, recorded with its message and retried once, and the run continues. ## [2.12.1] - 2026-09-20 Phone fixes for `/bench/run` from the first iPhone attempts under v4 (site only; no package changes). ### Fixed - **The runner page fits a phone again.** A lane's unavailability reason (on iOS Safari, the sentence explaining that wllama's model cache needs the Origin Private File System, with the browser's error inside) was rendered in a no-wrap badge inside a non-shrinking column, so the row and the whole page grew to about three viewport widths and nothing wrapped. Reasons and notes now wrap under the lane name, the badge only says "unavailable", lane names wrap instead of truncating, and the wllama reasons are shorter. Verified in WebKit at the iPhone 15 viewport: page width 393 px, no element past the edge (the badge had reached 1123 px). - **A run that ends with a page reload can be diagnosed from the phone.** The saved attempt now records the phase of the lane that was running (load, warmup, reload, or the timed work) as well as its id, the recovery card says "ended during `` (``)", and a "Copy diagnostics" button puts a plain-text summary (suite, harness, finished cells with their errors, the cell and phase running when the page ended, browser, OS, GPU, WebGPU, storage quota) on the clipboard, with an inline text box as the fallback. Mobile browsers reload a page that exceeds its memory budget without leaving a trace; the iPhone reload reported today happened on every attempt, with and without the quality lane, and this is the record needed to pin it to a lane. - **The overlay's step number follows the checklist.** "Step N of M" came from the runner's cell counter, which does not count skipped cells or warm reloads, so it drifted from the "done" counter (the M4 Thorough run showed "Step 60 of 69" beside "62 of 69 steps done"); it now uses the same ordered plan as the checklist, and a warm reload keeps the group's step in the header with a "Reloading … from the cache" status line instead of "Preparing the run". ## [2.12.0] - 2026-09-20 Protocol bump to `localmode-bench/4` from the first two runs measured under v3 (a Quick and a Thorough suite on the M4 Mac mini, both green except the three cells below), plus the runner fixes those runs surfaced. **Released:** `@localmode/wllama` 3.4.0 · `@localmode/bench` 0.6.0. ### Changed - **The llama.cpp lanes load every language model as text only** (`@localmode/bench` 0.6.0, protocol `localmode-bench/4`). The wllama provider attaches the vision projector its catalog lists for Gemma 4 E2B, so under v3 both llama.cpp lanes loaded a 557 MB projector the text-only workloads never use: its download and CLIP warmup ran inside the untimed warmup, the provider turned wllama's model cache off for the two-file source (the warmup and the warm reload re-downloaded the 3.46 GB weights), and on the CPU lane the pair did not fit the 4 GB wasm32 heap: `clip_model_loader::warmup` aborted with "insufficient memory (attempted to allocate 157.81 MB)" and all three `wllama/gemma-4-e2b` cells errored, retried, and errored again on the Thorough run. Loaded alone, the same GGUF runs on the CPU lane (36 layers, "offloaded 0/36 layers to GPU", 24 tokens in 2.8 s on the M4). The bench adapters pass the new `vision: false` and record `mmproj: false` in every wllama language cell's `runtimeConfig`. Only the Gemma 4 E2B pairing measured differently under v3; the version is bumped anyway so no leaderboard row mixes the two configurations, and the two v3 runs stay archived as v3. - **`@localmode/wllama` 3.4.0: `vision: false`** loads a catalog vision model (Gemma 4 E2B/E4B, Holo2) without its projector: no projector download, no projector memory, wllama's model cache stays enabled, `supportsVision` reports false. `resolveMmprojUrl()` is exported. ### Fixed - **Warm-reload cells read the reloaded instance's backend and `runtimeConfig` before it is disposed** (`@localmode/bench` 0.6.0); they were read after, which an adapter reporting lazily off a provider that forgets its load report on unload would have turned into stale values. wllama warm-reload cells still carry `offloadedLayers: "unreported"` by design: the warm reload times the provider's preload path (an OPFS cache probe, 340 to 420 ms on the M4), and llama.cpp loads in the untimed warmup of the timed cells, whose `warmupMs` is the real load-from-cache figure. - **Skipped lanes cost no cooldown** (`@localmode/bench` 0.6.0). Every model group was followed by the policy cooldown (5 to 10 s) and the pressure gate even when all of its cells were skipped, so a Thorough run with most lanes switched off idled for minutes before its first model loaded. - **The download estimate counts a shared model file once.** The two llama.cpp lanes load the same GGUF from one provider cache; the runner's "est. download" summed it twice. - **The run overlay's step line follows the model being loaded.** A model group's download and warmup happen before its first timed cell starts, so during a load the overlay kept showing the step that had just finished ("Step 30 of 69: Universal Sentence Encoder" with "downloading 95%" beneath it while LiteRT's Qwen3 was loading); the step line, counter, and status now switch to the lane being loaded, warmed up, or reloaded as soon as its first progress event arrives. ## [2.11.0] - 2026-09-20 Protocol bump to `localmode-bench/3` after the laptop submissions revealed that the wllama lane had been running on WebGPU while labelled WASM, plus the fixes the same batch of runs asked for. **Released:** `@localmode/wllama` 3.3.0 · `@localmode/bench` 0.5.0. ### Fixed - **`@localmode/wllama` 3.3.0: `useWebGPU: false` now keeps inference on the CPU, and `gpuAccelerated` tells the truth.** wllama 3.5 offloads every layer to WebGPU by default (`n_gpu_layers` 99999) wherever `navigator.gpu` exists; the provider forwarded nothing for `useWebGPU: false` (so it changed nothing) and computed `gpuAccelerated` from the request. It now passes `n_gpu_layers: 0` for `false`, documents `'auto'` as the effective default, captures llama.cpp's own `load_tensors: offloaded N/M layers to GPU` line through a logger handed to wllama, and reports `gpuAccelerated` and the new `offloadedLayers` from it once loaded (the embedding model gains both; the reranker honors the settings). - **Bench protocol v3: the wllama lane is split.** Every v2 wllama cell measured on a WebGPU-capable browser is a llama.cpp-WebGPU number recorded as `wasm` (on an Apple M4: SmolLM2 decode 612 chars/s where the CPU path measures 279, TTFT 71 ms vs 668 ms), and its comparison against the Transformers.js WASM lane was GPU against CPU. v3 pins `wllama` to the CPU and adds `wllama-webgpu` (every layer offloaded) over the same GGUF files, derived from the wllama catalog entries so the two cannot drift; the recorded backend follows llama.cpp's offload report, never the request; every cell carries a `runtimeConfig` (threads, GPU layers requested, offload report, device, dtype). The methodology page carries the v3 entry; archived v2 runs stay published as v2. Verified in real Chrome on the M4: the CPU lane logs "offloaded 0/31 layers", the WebGPU lane "31/31", and the run file records `wasm` / `webgpu` with the matching `runtimeConfig`. ### Added - **The runner cannot get stuck, and you can see that it is alive.** Runs on several lab devices appeared frozen: the status line sat on one iteration with nothing changing. Two causes, two fixes. (1) The Transformers.js WASM lane computed on the page's main thread, so during a generation the page could not repaint at all; that lane now runs inside a dedicated worker (`src/lib/bench/transformers-worker.ts`, its own ONNX runtime instance, model files from the shared Cache API, `runtimeConfig.worker: true`), verified in real Chrome with Qwen3 0.6B: the page updated every second through both timed cells (32 chars/s decode, incremental stream, warm reload 42 s). (2) A lane that genuinely stops (a worker that died, a lost WebGPU device, a hung fetch) is now caught by the harness watchdog (`@localmode/bench` 0.5.0): 2 minutes without a streamed chunk or 3 without load progress aborts the cell, it is retried once with the failed attempt kept on the cell, then skipped, and the run continues to completion and auto-submits. The Progress card shows a live activity line (a pulse that keeps moving even while the main thread is busy, elapsed time in the cell, the current phase, characters and chunks streamed so far, and "last progress N s ago"), an explanatory note after 45 s of silence that distinguishes a session being built from a stall, the watchdog's retry notices, and a "retried ×N" marker in the results table. - **A hidden tab no longer costs the whole cell.** Browsers throttle background tabs, so a timed iteration taken while the tab was hidden measured the browser's scheduling; the runner used to mark the whole cell invalid and move on. It now sets that iteration aside (kept on the cell as `discardedIterations`), waits up to 10 minutes for the tab to be visible again, and repeats it; nothing starts timing while the tab is hidden. The overlay says "Paused: this tab is not in front" while waiting and explains that hidden measurements are repeated. - **A run overlay for participants.** While a run is in progress, everything moves into one modal over the page: a clear instruction to keep the tab open, visible, and in front (with the consequence of not doing so, and for paid-study sessions a note that the completion code appears on this page after the upload), overall progress with an estimated time remaining and clock time (measured from this run's own cell durations and observed download rate, priors before that, labelled rough until the first models have run), the current step in plain language (model, workload, iteration, characters streamed, "last progress N s ago"), download bars, the watchdog's retry notices, a per-model checklist in execution order, a warning if the tab went to the background (those measurements are marked invalid), and a Cancel button. Leaving or reloading the page mid-run asks for confirmation. The overlay closes when the run finishes and the results table takes its place. - **Every cell records its runtime configuration** (`@localmode/bench` 0.5.0 `runtimeConfig`, copied from the adapter's load result onto each cell of the group and onto warm-reload cells): the per-cell equal-care record the methodology asks for. Transformers.js lanes record device, dtype, and whether they ran in the worker; wllama lanes record thread count, GPU layers requested, WebGPU adapter presence, llama.cpp's offload report, and the prompt-cache pin. ## [2.10.0] - 2026-09-19 Closes the gaps the first laptop and phone submissions exposed: a device whose long runs died with the tab and left nothing to diagnose, a wllama WASM abort recorded without its location, a thorough run that silently held only the lanes its submitter left enabled, and a trace bloated by one pressure sample per second. **Released:** `@localmode/bench` 0.4.0. ### Added - **Crash-resilient partial runs.** The runner now writes the environment capture and every finished cell to IndexedDB as the suite progresses (`src/lib/bench/partial-run-store.ts`, over the new `onEnvironment` and existing `onCellFinish` hooks). When a tab dies mid-suite, which is what a Standard or Thorough run does on a machine past its memory ceiling, the next visit to `/bench/run` shows the unfinished attempt (suite, cells finished of planned, the cell that was executing) with "Export partial run" and "Discard". The export is the run-file shape marked `partial: true` with the list of unfinished cells; partial runs are never published. Covered by a bench e2e lane that kills the page mid-run and recovers the record in a fresh page. - **Every suite cell is in every result.** Lanes the submitter switches off and lanes the device cannot run (no WebGPU, a GPU-compiled build without a GPU) are planned with a skip reason and recorded as `skipped` cells with that reason, instead of being dropped, so a run's suite label describes what was attempted and what was not (`@localmode/bench` 0.4.0 `PlannedCell.skipReason`). - **WASM aborts are diagnosable.** Error cells keep the wrapped provider error's `causeName` and `causeStack` (capped) beside `cause`; wllama's `RuntimeError` "(ABORT) " names the failing llama.cpp frame only in that decoded stack. - **Gemini Nano downloads on Run.** The Chrome Built-in AI lane used to be disabled whenever Chrome reported Gemini Nano as merely "downloadable", leaving the submitter to trigger the download by hand. Chrome starts that one-time, browser-wide download only from a user activation, so the Run click now starts it synchronously (`src/lib/bench/chrome-ai-download.ts`) while the other lanes run; the lane is enabled with a "downloads once when you click Run" note, its progress shows in the Progress card, the chrome-ai adapter waits for the download before its cells (recorded cold, load measuring the remaining wait), and a refused or failed download becomes an error cell, never a silent skip. Unit-tested against a fake Prompt API (`scripts/bench-chrome-ai-download.test.ts`); the real download path needs Chrome 148+ with Gemini Nano and stays on the manual real-Chrome sweep. ### Fixed - **`pressure-change` events fired every second regardless of state**, 1,662 of them in one thorough run; they are now recorded on state transitions only, while the pressure gate still reads every sample. ## [2.9.1] - 2026-09-19 **Released:** `@localmode/bench` 0.3.1. ### Fixed - **`gpuModel` on WebKit read "apple".** WebKit fills the WebGPU adapter description with the bare vendor token and bench 0.3.0 preferred that over the WebGL renderer string, so the first iPhone submission under 0.3.0 (run c5b06059) recorded `gpuModel: "apple"` where the WebGL channel says "Apple GPU". The description is now used only when it names something the vendor and architecture tokens do not (`resolveGpuModel()`), otherwise the WebGL renderer string is parsed as before. Additive; no schema or protocol change. ## [2.9.0] - 2026-09-19 Makes every benchmark submission record the full device identity the browser discloses (so later analyses never depend on a field that was not kept), stamps runtime versions into runs, and fixes the WebKit isolation and mobile-suite problems surfaced by the first iPhone submissions. **Released:** `@localmode/bench` 0.3.0 (folds in the unpublished 0.2.1 UA-parse fix). ### Added - **Every benchmark submission now records everything the browser discloses about the device** (`@localmode/bench` 0.3.0, additive schema fields, no protocol bump; archived runs stay valid). Beyond the device class, a run now carries the raw user-agent string, browser engine/vendor/`webdriver`, OS version, architecture, bitness and model where disclosed, a derived form factor (phone/tablet/desktop; iPads that report as Macs are unmasked by touch points), touch/pointer/display-mode signals, the JS heap ceiling, WebGPU adapter features, limits, subgroup sizes and WGSL language features, a full WebGL identity block and a GPU model parsed out of the renderer string ("Apple M4", "NVIDIA GeForce RTX 4070"), the WebAssembly proposal matrix (22 features probed with the wasm-feature-detect 1.9.0 detection modules, inlined), presence checks for every API the runtimes depend on (WebGPU, WebNN, OPFS, Cache API, workers, Compute Pressure, Chrome Built-in AI verdicts, ...), storage usage details, battery, Network Information, display (color depth, orientation, HDR, wide gamut, color scheme), locale and time zone, page origin and visibility. Verified in real Chrome 145, WebKit 26.5 and Firefox 151 with zero console errors. - **Runtime versions are stamped into every run.** `next.config.mjs` resolves the installed versions of the provider packages and the inference runtimes they wrap at build time (and wllama's CDN pin, which is what actually executes), and the runner records them as `harness.runtimeVersions`, each cell as `runtimeVersion`; the build commit is recorded when the host exposes it (`VERCEL_GIT_COMMIT_SHA`). Runner harness version 0.3.0. - **`/bench` shows a "Recent submissions" table** with the disclosed device identity per run (form factor, GPU model, browser + engine, OS + architecture, cores, memory, storage quota, isolation, WebGPU) linking to the run's JSON in the dataset; the leaderboard index (`index/summary.json`) carries these fields plus the harness and runtime versions so future analyses do not need to re-read every run file. ### Fixed - **`hardware.coresClamped` was always true on Chrome.** The label compared the UA-CH brand ("Google Chrome", "Microsoft Edge") against "Chrome"/"Edge" and so marked every Chromium submission's core count as clamped; it now follows the rendering engine (Chromium reports real cores; Gecko and WebKit clamp or randomize). - **WebKit browsers were never cross-origin isolated.** Safari, and every browser on iOS, does not implement `Cross-Origin-Embedder-Policy: credentialless`, so the site's pages ran without SharedArrayBuffer there: single-threaded WASM, a 1 ms timer, and wllama unable to create its shared WASM memory. WebKit user agents now receive `require-corp` (Chromium keeps `credentialless`); verified on WebKit 26.5 that the page is isolated, the timer drops to 20 µs, the Transformers.js and MediaPipe WASM lanes run multithreaded, and no cross-origin asset is blocked. - **wllama lane on WebKit: honest unavailability instead of five error cells.** wllama's model cache needs the Origin Private File System; WebKit contexts that cannot open it throw `DOMException UnknownError` before any WASM runs (the iPhone runs recorded this as "Out of memory" on every wllama cell). The bench adapter now probes both the shared 4 GB WASM memory wllama imports and OPFS, and marks the lane unavailable with the reason; the runner shows it before Run. - **Phones and tablets are gated to the Quick suite.** Standard and Thorough stack several runtimes' WASM heaps in one page and mobile browsers kill the tab first, which lost whole runs on an iPhone; the suite picker now disables them on mobile with an explanation. - **`@localmode/bench` 0.2.1:** UA-parse fallback names Chrome/Firefox for iOS and reports the iOS version (submissions had arrived as browser `unknown`). ## [2.8.0] - 2026-09-19 Adds the paid-study session flow to the bench runner for the crowdsourced data collection and fixes a leaderboard caching lag surfaced by the first live protocol-v2 submission. No npm package releases; `apps/ui` only. ### Added - **`/bench/run` paid-study session support.** When the page is opened with `?PROLIFIC_PID=&cc=`, the run records `prolific:` in `environment.userReportedDevice` (the raw id is never stored or published) and, once the run finishes and the automatic upload attempt has resolved, shows the completion code with a link to Prolific's completion page; if the upload failed, it still shows the code with a note to message the researcher (payment is on attempt). A pre-run note tells the participant the code will appear on this page. Covered by the bench e2e real-run lane, which also asserts the raw id never reaches the exported payload. ### Fixed - **`/api/bench/leaderboard` could serve a pre-submission copy for up to an hour.** The response allowed `stale-while-revalidate=3600`; the edge kept returning the old aggregate 27 minutes after the first v2 run was published while the ISR page had already refreshed. The stale window is now one revalidation period (300 s), matching the page. ## [2.7.0] - 2026-09-19 Bumps the LocalMode Bench protocol to `localmode-bench/2` after three real-Chrome thorough-suite pilots surfaced measurement defects in the harness and two provider bugs underneath it. The third pilot ran all 51 cells green. **Released:** `@localmode/bench` 0.2.0 · `@localmode/wllama` 3.2.0 · `@localmode/transformers` 4.1.2. ### Fixed - **`@localmode/transformers` 4.1.2 - `preloadModel()` leaked a resident ONNX session.** It built a full pipeline only to populate the model-file cache and never disposed it; ONNX Runtime's WASM heap never shrinks, so after a few large models every later session creation failed with `std::bad_alloc` - in the bench this killed all 18 Transformers.js cells of a thorough run regardless of execution order. The throwaway session is now disposed, and a new `device` option replaces the hard-coded WebGPU (defaulting to WebGPU only when `navigator.gpu` is present, else WASM). - **`@localmode/wllama` 3.2.0 - a bare `prompt` is now a user turn, like every other provider.** Previously a `prompt` with no `messages`/`systemPrompt` bypassed the GGUF chat template (raw completion) and returned the whole generation as one chunk; instruct models fed untemplated text often emit EOS immediately (SmolLM2-135M produced 0 characters). Bare prompts now go through `createChatCompletion` with real token streaming when the GGUF ships a template; `providerOptions.wllama.raw` keeps raw completion, and base GGUFs without a template use it automatically. `cache_prompt` and `chat_template_kwargs` pass through. - **`@localmode/bench` 0.2.0 - protocol `localmode-bench/2`.** Stream-coherence gating (TTFT/decode only from genuinely incremental traces; LiteRT-LM's terminal-burst surface produced an artifact 693,902 chars/s decode rate under v1 and now reports an honest end-to-end rate marked `e2e`); quality lane rebuilt (48-token budget, `` stripping, uniform per-pairing no-think suffixes, stored raw outputs, server-side recomputation, and a surfaced parse rate so a format-limited score is never read as low fidelity); a degenerate-generation gate; the wllama lane pins `cache_prompt: false` so repeated prompts pay prefill every iteration (llama.cpp's prompt-KV reuse had dropped TTFT from 1018 ms to 24 ms from the second iteration on); error causes preserved on cells; and a deterministic runtime execution order for reproducibility. The leaderboard and run pages surface `e2e` rates and `(N% parsed)` annotations; the analysis CSV tooling gains `overallCharsPerSec`/`streamIncremental`/`qualityParseRate` columns. The leaderboard aggregates only runs measured under the current protocol, so archived v1 runs stay published as v1 (never re-scored) but leave the leaderboard until v2 submissions arrive. Known limitation, documented rather than fixed: the Transformers.js lanes share one ONNX Runtime WASM heap per page that never shrinks, so under system memory pressure a large-model session can fail with `std::bad_alloc` and later ORT sessions in that page fail too; such cells are recorded as errors (with cause and a failure-time memory sample), never as data, and the execution order does not change this. ## [2.6.0] - 2026-09-18 Introduces **LocalMode Bench** - an open, cross-runtime benchmark of in-browser AI with a public leaderboard at [localmode.ai/bench](https://localmode.ai/bench) - and fixes a wllama preload crash on encoder-only GGUF models. **Released:** `@localmode/bench` 0.1.0 · `@localmode/wllama` 3.1.2. ### Added - **`@localmode/bench` 0.1.0 - the LocalMode Bench measurement harness.** Versioned protocol `localmode-bench/1` with MLPerf-Client-compatible metrics (TTFT; pp128/pp512 prefill; tg128 decode excluding the first token), a suite runner that records raw per-chunk timing traces and full environment captures, integrity validation (canonical-JSON SHA-256 digests, timer-grid conformance, plausibility envelopes, software/virtual-renderer rejection), a quality-fidelity lane (tinyMMLU accuracy, STS-B Spearman), and leaderboard aggregation + CSV tooling that recomputes every published statistic from the raw traces. Zero runtime dependencies; runtimes are injected via adapters. - **localmode.ai/bench - the public benchmark surface.** The leaderboard (ISR over the open CC0 [LocalMode-Bench](https://github.com/LocalMode-AI/LocalMode-Bench) dataset), the in-browser runner at `/bench/run` (suite/lane picker with availability preflight, live progress, JSON export, one-click submission; models download only behind the explicit Run action), the versioned protocol page at `/bench/methodology`, and the submission APIs (HMAC session nonce, server-side trace recompute, public quarantine for flagged runs). Model pairings run the same weights family across WebLLM, wllama, Transformers.js (WebGPU and WASM lanes), LiteRT, and Chrome Built-in AI - headlined by Qwen3-0.6B across five lanes. - **localmode.dev/docs/bench** - a docs pointer page linking the leaderboard, runner, methodology, dataset, and harness package. ### Fixed - **`@localmode/wllama` `preloadModel()` crashed the WASM runtime on encoder-only GGUFs** (embedding and reranker models). The previous implementation preloaded through a full `loadModelFromUrl()`, whose causal-LLM init warmup aborts in `llama_context::output_reserve` on encoder-only architectures - and loaded full model weights into memory just to populate the cache. Preloading now routes through wllama's `ModelManager` download-only path (regression tests in `packages/wllama/tests/preload.test.ts`). ## [2.5.0] - 2026-07-11 Makes on-device structured output actually reliable — small models now emit schema-conforming JSON through grammar-constrained decoding and stronger prompts — and refines the `@localmode/ui` platform at localmode.ai: Device Badge folds into the local-first family, "Open in v0" is supported, the component browser is deep-linkable, and the image blocks are WASM-pinned for stability. **Released:** `@localmode/core` 2.4.2 · `@localmode/react` 2.4.0 · `@localmode/webllm` 2.2.0. ### Fixed - **Structured output was broken under Zod 4 and unreliable on small models.** `@localmode/core`'s duck-typed Zod→JSON-Schema reader only understood Zod 3 internals, so every Zod 4 scalar collapsed to `{ type: 'object' }`; it now normalizes both layouts (and adds `bigint`). `buildStructuredPrompt()` now emits a concrete filled example and an explicit top-level-key list, so a model returns data instead of echoing the schema. - **Image blocks wedged on WebGPU.** The background-remover (SegFormer) and image-enhancer (Swin2SR) pin `device: 'wasm'` — the ONNX-Runtime WebGPU session lifecycle wedged on cancel-mid-load and on switching super-resolution modes, while these tiny models run in ~1s on WASM. The object-detector pauses its live face-tracking loop during one-shot DETR so the two don't starve the GPU. - **Accessibility & hydration** — `before-after-image-viewer` is now a keyboard-navigable ARIA tablist, and `event-log-viewer` moved its `Date.now()` clock into an effect to fix a server/client hydration mismatch. ### Added - **Schema-constrained JSON generation.** `@localmode/webllm` forwards `providerOptions.webllm.response_format` to MLC for XGrammar-constrained decoding, and `useGenerateObject` (`@localmode/react`) gains a `providerOptions` passthrough — together forcing schema-conforming JSON from small models (wired into the Data Extractor block). - **"Open in v0" support** — a new `add-default-export` registry-build step appends `export default ` to shipped component payloads (v0 default-imports the primary component), shown only for an empirical hand-verified allowlist of primitives that render real UI in v0. - **Deep-linkable component browser** — `/docs/components` accepts `?filter=` and reflects the active family into the URL; the docs homepage gained a "100+ components" preview and a "36 interactive blocks" section linking the real `/blocks//` routes. ### Changed - **Device Badge moved into the local-first family** — `ui/device-badge` → `ui/local-first/device-badge` (source, install command, and docs route `/docs/local-first/device-badge`, with a 308 from the old path). The registry's lone top-level "seed" is gone; every component is now family-scoped. - A mounting preview (e.g. a cmdk list) can no longer scroll the component-browser page, and several blocks now render their full surface before the model loads (controls stay disabled until ready). ### Removed - `ui/local-first/vector-import-flow` no longer depends on `format-detection-badge` — it inlines a minimal fallback and installs independently. ## [2.4.0] - 2026-07-09 Repairs Chrome Built-in AI, which had been unreachable on every modern Chrome, and adds the user-activation download gate Chrome requires before it will fetch an on-device model. **Released:** `@localmode/core` 2.4.0 · `@localmode/chrome-ai` 2.2.0 · `@localmode/react` 2.3.0. ### Fixed - **Chrome Built-in AI was unreachable on every modern Chrome.** `@localmode/chrome-ai`'s Summarizer and Translator, and `@localmode/core`'s four `is*APISupported()` capability detectors, all read the legacy `self.ai.*` namespace that Chrome has removed. `isChromeAISupported()` was literally `'ai' in self`, so it returned `false` on exactly the browsers where the APIs exist — and `detectCapabilities().features.chromeAI` was always `false`. All now read the modern top-level `self.Summarizer` / `self.Translator` / `self.LanguageModel` globals, with the legacy namespace as a fallback. - **Chrome's `SummarizerType` enum is `'tldr'`, not `'tl;dr'`.** Passing `'tl;dr'` made Chrome throw a `TypeError`, which the provider-fallback probe then swallowed and reported as "this browser does not support it" — blaming the browser for a caller bug. The enum value is corrected across `@localmode/chrome-ai` and `@localmode/react`, and the probe now rethrows bad-option errors instead of mislabelling the browser. - The Prompt API requires **Chrome 148+** for web pages (Chrome 138 shipped it for extensions only). Summarizer and Translator remain Chrome 138+. Documentation and runtime error hints corrected throughout. ### Added - `useProviderFallback` (`@localmode/react`) exposes `chromeAvailability`, `refreshChromeAvailability`, `requestChromeDownload`, `chromeDownloadProgress`, and `downloadingCapability`, plus standalone `probeChromeAvailability` / `downloadChromeModel`. Chrome only starts its one-time, browser-wide model download from a **user activation**, so the download must be triggered from a click. - Chrome AI Summarizer and Translator gained `allowDownload` + `onProgress` settings and an `availability()` gate, matching the language model; they now throw typed `SummarizationError` / `TranslationError`. - New UI registry primitive `ui/local-first/chrome-ai-download-gate` (`ChromeAIDownloadGate` + `ChromeAIReadyBadge`) rendering the download button, progress, and terminal states. Wired into the `writing-tools/{write,translate,summarize}` blocks. The catalog is now 107 components across 10 families (147 registry items). ## [2.3.0] - 2026-07-09 Launches the `@localmode/ui` registry platform at localmode.ai — a copy-owned catalog of 106 AI UI components across 10 families, plus 37 composed blocks across 12 gallery categories that wire those primitives to real on-device models. The `apps/showcase-nextjs` demo app is retired (absorbed into the blocks at parity) and the built-in DevTools widget is removed. Alongside the platform: a `@localmode/core` RAG ingest ⇄ search round-trip fix, agent tool approval, a StorageAdapter conformance suite, cross-session persistence fixes across all three storage adapters, a resilient model-file cache, and provider load fixes for WASM VLMs, long-context wllama, and LiteRT. **Released:** `@localmode/core` 2.3.0 · `@localmode/react` 2.2.0 · `@localmode/transformers` 4.1.0 · `@localmode/wllama` 3.1.0 · `@localmode/langchain` 2.1.0 · `@localmode/devtools` 3.0.0 (breaking) · `@localmode/litert`, `@localmode/dexie`, `@localmode/idb`, `@localmode/localforage` 2.0.1. ### Added — @localmode/core - **`ingest()` object call form + `abortSignal`** — `ingest({ db, documents, model?, embedder?, ...options })` joins the positional `ingest(db, documents, options?)` as a TypeScript overload; passing an `EmbeddingModel` as `model` generates chunk embeddings via `embedMany()`. `IngestOptions` gains `abortSignal`, and both forms throw actionable errors on a missing `db` or non-array `documents`. New type: `IngestObjectOptions`. - **`TEXT_METADATA_FIELD`** — the metadata key (`'_text'`) under which ingestion stores chunk text, now a shared constant consumed by both the write side and `semanticSearch()`'s read side so the two cannot drift. - **`defineTool()`** — identity helper anchoring `ToolDefinition` generics so `parameters` and `execute(params)` type-check against each other, letting typed tools fit `ToolDefinition[]` without casts. - **Agent tool approval** — opt-in human-in-the-loop gate for the ReAct loop: flag a tool `requiresApproval` and supply `onToolApproval` (on `AgentConfig`, per-run override on `AgentRunOptions`). Denials skip execution and feed the reason back as the step observation; decisions are recorded on `AgentStep.approval`, and a flagged tool with no callback fails fast. New types: `ToolApprovalRequest`, `ToolApprovalDecision`. - **`createKnowledgeBaseEngine()`** — a `kind: 'core'` engine implementing the new frozen `KnowledgeBaseEngine` contract: chunk (off/recursive/semantic) → embed → typed-metadata VectorDB, vector `search`, and grounded streaming `ask` with reasoning stripped and PDF page attribution. Models are injected, so core gains a RAG engine with no new dependency. - **`streamEmbedManyImages()`** — streaming batch image embedding mirroring `streamEmbedMany()`: per-image yields, `onBatch` progress, `batchSize`/`adaptiveBatching`, per-batch AbortSignal checks and retry. - **`createStorageAdapterConformanceSuite()`** — a framework-agnostic, dependency-free StorageAdapter contract suite (21 cases: full-`Collection` fidelity, document/vector/index ops, close→reopen persistence, SQ8 cross-session fidelity). The factory supplies a `reopen()` handle, so the suite catches adapters that look fine in-session and corrupt on reopen. Adopted by all three external adapters. - **`createMockRerankerModel()`** — deterministic mock `RerankerModel` (configurable `scores`/`scoreFn`, honors `topK`, abortable `delayMs`, recorded `calls`). - **`KMeansOptions.random`** — injectable random source for deterministic `kMeansCluster()` runs (default `Math.random`, behavior unchanged). ### Added — @localmode/react - **8 new hooks (56 → 64)**: `useModelLoad` (provider load lifecycle with normalized 0–1 progress and a warmup-driven status registry), `useRerank`, `useEncryptedVault` (passphrase-locked AES-GCM CRUD over a core `StorageAdapter`, key in memory only), `useProviderFallback` (per-capability Chrome Built-in AI ⇄ Transformers.js resolution), `usePhotoLibrary`, `useKnowledgeBase`, `useObjectUrl`, and `useStreamingTracker` (experimental). All resolve providers by injection or dynamic `import()`, so `packages/react` gained no provider dependency. - **`useAgent` tool-approval surface** — returns `pendingApproval` plus `approve()`/`deny(reason?)` while the ReAct loop is paused on a gated tool. Ungated runs never surface one. - **`useChat` additions** — per-turn `usage` + cumulative `totalUsage`, lifecycle `status`, `streamingMessageId`, `setMessages()`, and `regenerate()` with selectable reply variants. - **`useEmbedManyImages` progress parity** — now streams via `streamEmbedManyImages()`, exposing `progress: { completed, total }` and a `batchSize` option. - **Additive options and richer returns across existing hooks** — `useSemanticSearch`, `useGenerateText`, `useSynthesizeSpeech`, `useTranscribe`, `useClassifyZeroShot`, `useLiveTranscribe`, `useTurnTaker`, `useStreamSpeech`, `usePipeline`, `useReindex`, `useAuditLog`, `useSemanticCache`, `useVoiceRecorder`, `useCapabilities`, `useStorageQuota`; `useSequentialBatch`/`useBatchOperation` publish results incrementally with per-item errors. Re-exports `getTextContent`/`normalizeContent` from core. ### Added — @localmode/transformers - **Resilient model-file cache (default on)** — a custom Transformers.js cache over the browser Cache API storage the provider already uses (`transformers-cache`) whose write path can never fail a model load: a failed write serves the fetched response and warns once per URL, and no-`caches` environments keep stock behavior. Kills the intermittent `NetworkError: Cache.add() encountered a network error` failure class. Opt out with `createTransformers({ resilientCache: false })`. ### Added — @localmode/wllama - **GGUF model discovery** — `searchGGUFModels()` and `listGGUFFiles()` browse the 160,000+ GGUF repos on the anonymous HuggingFace API and list a repo's `.gguf` files with parsed quant labels. Failures surface as a typed `HFApiError` (`rate-limit` / `network` / `not-found`). ### Added — @localmode/langchain - **`createLangChainKnowledgeBaseEngine()`** — a `kind: 'langchain'` engine implementing the same core `KnowledgeBaseEngine` contract via the real `LocalModeEmbeddings`/`LocalModeVectorStore`/`ChatLocalMode` adapters, result-equivalent to core's engine. Models are injected, so consumers who never toggle LangChain never pull it. ### Added — @localmode/devtools - **`/react` hooks subpath** — 9 hooks over the bridge snapshots (`useDevToolsBridge`, `useDevToolsStatus`, `useDevToolsQueueStats`, `useDevToolsEvents`, `useDevToolsModelCache`, `useDevToolsPipelineRuns`, `useDevToolsVectorDBs`, `useDevToolsStorage`, `useDevToolsCapabilities`), built on `useSyncExternalStore` with SSR-safe inert values, preserved snapshots after `disableDevTools()`, and late-enable attachment. The main entry stays React-free. ### Added — @localmode/ui - **`@localmode/ui` registry platform (localmode.ai)** — a single Next.js 16 / React 19 app that is BOTH a shadcn registry endpoint AND a Fumadocs docs site, distributing copy-owned, composable AI UI primitives ("LocalMode Elements"). Not an npm package: components install with the shadcn CLI (`npx shadcn add @localmode/ui/`) and the consumer owns the copied `.tsx`. shadcn/ui CSS-variable theming (Tailwind 4), generated `ui/all` + per-family aggregates, an MCP-readable `/registry.json` catalog, optional token-gating, and a Run-gated `` that downloads no model until clicked. - **146 registry items** — 106 copy-owned components across 10 families (Conversation 24, Local-First 24, Results & Insights 12, Input Controls 11, Audio 10, Media & Vision 7, Data & Documents 5, Security & Privacy 5, Artifacts & Canvas 4, DevTools 4), 3 internal `ui/lib/*` items (`utils`, `browser-utils`, `use-environment`), and 37 blocks. Primitives are presentational and hook-driven, and install with **zero `@localmode/*` packages**: generic browser helpers come from the copy-owned `ui/lib/browser-utils` item and the navigator-reading hooks from `ui/lib/use-environment`. A consumer-test lane guards the invariant; blocks are the sole carve-out, guarded by an inverse lane. - **37 composed blocks across 12 route-served gallery categories** — live, full experiences that wire the primitives to real on-device models, served at the public `/blocks` gallery and installable as `registry:block` items. Categories: `chat`, `knowledge` (4), `audio` (6), `vision` (2), `text` (1), `device` (3), `writing-tools` (4), `text-insights` (4), `photo` (4), `image-studio` (3), `privacy` (2), `agents` (2), plus the `devtools-drawer` layout chrome. Blocks are the wiring layer and the ONLY registry items allowed `@localmode/*` dependencies; each gallery block ships as a single self-contained, copy-paste-ready file, and every model load is gated behind an explicit in-block action. - **Accessibility floor across every block and primitive** — correct roles, accessible names, keyboard operability, WCAG-AA contrast, visible `focus-visible` rings, an ARIA tablist for the Preview/Code tabs, `role="status"` live regions, and `role="alertdialog"` destructive confirms. Block sources are testid-free (E2E selects via role/label/text), and a shared `stripSnippet()` AST transform guarantees every shipped block file has zero `data-testid`, zero QA comments, and a ≤3-line header. - **Registry dependencies ship as absolute URLs, so "Open in v0" resolves them.** Items are authored with namespaced `registryDependencies` (`@localmode/ui/lib/utils`), which resolve only through a consumer's `components.json` registries map — the shadcn CLI has that map, v0 does not. The final `registry:build` step rewrites every namespaced dependency in the emitted `public/r/**.json` into an absolute `/r/ui/.json` URL (618 across 153 items); bare shadcn names pass through and the step is idempotent. The origin comes from `NEXT_PUBLIC_REGISTRY_ORIGIN ?? NEXT_PUBLIC_SITE_URL`, so **a deploy must set one at build time**. ### Added — apps/ui - **Block-page chrome** — a persistent category sidebar (with a mobile disclosure), a breadcrumb, and per-page "Copy page" / "View as Markdown" actions. - **Markdown export** — docs pages expand previews, type tables, and install tabs into markdown; block pages expose an `/api/blocks-md/` route with the full block source. - **PWA (installable + offline)** — a manifest, a Serwist service worker built postbuild (app-shell precache, model/CDN hosts NetworkOnly, `/offline` fallback), an `SWRegistrar`, and generated icons. - **Cross-origin isolation** — COOP `same-origin` + COEP `credentialless`, unlocking threaded WASM while keeping cross-origin model downloads working. - **Other chrome** — a `/capabilities` browser-support page, a live NetworkStatus pill on `/blocks`, a console suppressor for known WASM-runtime noise, custom `not-found`/`error` pages, and a new favicon. - **New env vars** — `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_REGISTRY_ORIGIN`, and optional verification / IndexNow / social vars (inert until set). ### Added — docs - **New guides** — a Next.js integration guide; "Model Caching & Reliability" (transformers) and "Model Caching & Offline" (LiteRT) sections; `use-with-ai-sdk.mdx` and `bring-your-own-data.mdx`; and launch content for `@localmode/ui`. ### Added — repo tooling - **ESLint works again** — the repo had no ESLint config and the root `pnpm lint` used the removed `--ext` flag. A flat `eslint.config.mjs` now lints `packages/**` (apps keep their own Next.js configs) with `eslint-plugin-react-hooks` registered. New `pnpm lint:fix`. - **`pnpm test:types`** — compiles the type-level contracts in `packages/core/tests/**/*.test-d.ts` (Vitest does not run them), guarding `jsonSchema` inference and `ToolDefinition[]` assignability. ### Changed — @localmode/react - `useModelStatus` de-stubbed — backed by the `useModelLoad` registry and reflecting the real load lifecycle, instead of optimistically reporting `isReady: true` as soon as an instance existed. - Published load progress is non-decreasing within a load attempt (high-water clamp, reset per `load()`); raw per-file byte counts are unaffected. - `toAppError()` carries a core `LocalModeError`'s `code` to `AppError.code` and appends its `hint` to the message. - `useReindex`, `useCalibrateThreshold`, and `useModelRecommendations` expose `error` as `Error | null` (was `{ message: string } | null`). ### Changed — docs - **Ingest/search examples normalized to the real API** across the core RAG and embeddings guides, getting-started, the adapter quick starts, and blog posts: object-form `ingest()` examples are now valid against the shipped overload, option names are corrected, and `semanticSearch` results read text from `results[].text`. - **Blocks-world sweep** — the 436 legacy `localmode.ai/` deep links across 99 content files were rewritten to their absorbing `localmode.ai/blocks/` URLs, and repo guidance updated to describe the blocks world. ### Fixed — @localmode/core - **`ingest()` → `semanticSearch()` text round-trip** — text extraction never checked the `_text` metadata key `ingest()` writes, so every ingested chunk came back with `text: undefined` and RAG flows building context from `results[].text` injected empty strings (a HIGH-severity real-consumer bug; search itself was never broken). Precedence is now `text`, `content`, `body`, `_text`, `__text`, `pageContent`; `streamSemanticSearch()` inherits the fix. - **`jsonSchema()` type inference** — `jsonSchema(schema)` now actually infers `T` from the Zod schema instead of collapsing to `unknown`, and `ToolDefinition.execute` uses method syntax so typed tools are assignable to `ToolDefinition[]`. - **Semantic cache never cached on the streaming path** — `semanticCacheMiddleware()` stored the response only after its `for await` loop, which never completes for consumers that stop at the `done` chunk (every `streamText()`/`useChat` turn), so streaming lookups always missed. The store now fires on `done`; a turn cancelled mid-stream still stores nothing. - **Agent runs no longer fail on reasoning models** (`Failed to generate valid object after 3 attempts`) — `generateObject()` appends the Qwen3 `/no_think` switch to the user prompt as well as the system prompt, ReAct action generations get an explicit 2048-token budget, and the action parser unwraps a schema-parroted single-element `oneOf`/`anyOf` wrapper. - `ModelLoadError`'s default message generalized to `Failed to load model: {modelId}` — the class is shared by every model domain, not just embeddings. - `StorageAdapter.getVector()`/`getAllVectors()` types now match the implementations (`Float32Array | Uint8Array`, the latter for SQ8/PQ payloads). ### Fixed — @localmode/react - Mid-flight cancellation is now silent for every `useOperation`-based hook, even when the wrapped core function turns an abort into a plain `Error` (e.g. `rerank()`/`classify()` "was cancelled"). - `cancel()` returns a hook to idle immediately — previously the loading state reset only when the promise settled, so a cancelled but non-interruptible in-worker call left the hook stuck loading. - `useVoiceRecorder` ignored microphone selection — new `deviceId`/`constraints` options are forwarded to `getUserMedia`, and recording now errors when the requested device is unavailable instead of silently falling back. ### Fixed — @localmode/transformers - **Cross-encoder reranking produced no ranking signal** — `doRerank` scored query and document independently, so the document never influenced the score and single-logit models collapsed to a constant `0`. It now encodes real (query, document) pairs via `AutoTokenizer` `text_pair` + `AutoModelForSequenceClassification`, with sigmoid on single-logit heads. - **Vision-language models failed to load on WASM** (`ERROR_CODE: 9`) — the hardcoded q4/fp16 multimodal dtype default uses ops onnxruntime-web only implements on WebGPU. The default is now device-aware: WebGPU keeps the q4/fp16 mix, WASM uses fp32 embed/vision with a q4 decoder. An explicit `settings.dtype` still overrides. - **English-only Whisper checkpoints (`*.en`) failed every transcription** — the force-injected `language: 'en'` / `task: 'transcribe'` defaults now apply only to multilingual checkpoints. ### Fixed — @localmode/wllama - **Long-context models no longer abort the wasm32 load** — the context length inferred from the catalog or GGUF metadata is capped at 8192 before `n_ctx`; models advertising native windows like 131072 requested a multi-GiB KV cache that cannot fit the wasm32 4GiB heap. An explicit `settings.contextLength` is never capped. - **Reranking works** — the pinned `@wllama/wllama@3.2.3` ships no rerank API, so every `WllamaRerankerModel` call failed with `createRerank is not a function` after the download. Bumped to `^3.5.1`, and the reranker loads in reranking mode. ### Fixed — @localmode/litert - `doGenerate()` no longer freezes the tab for the whole generation — it drains `sendMessageStreaming()` (per-token main-thread yields) instead of the synchronous `sendMessage()`. - CPU-capable models no longer fail to load in WebGPU-less browsers — the provider probes actual device usability and pins the CPU backend when `navigator.gpu` exposes no usable device. ### Fixed — @localmode/dexie, @localmode/idb, @localmode/localforage - **Full `Collection` persistence — quantization calibration, compression calibration, and drift fingerprints now survive a reopen.** All three adapters cherry-picked `{ id, name, dimensions, createdAt }` on collection write AND read, silently dropping the extended fields core stores, so quantized or compressed vectors round-tripped in-session but decoded as raw bytes after a close→reopen, and drift detection never fired. Collections now round-trip as the full object, and each adapter adopted the conformance suite. Data written with quantization/compression by earlier adapter versions is unrecoverable — clear and re-ingest. - **SQ8/PQ-compressed vectors now round-trip correctly.** The adapters coerced core's `Uint8Array` payloads to f32 (dexie/idb threw a `RangeError`; localforage returned a `Float32Array` of byte-values). Dexie/idb now persist the typed array itself, localforage adds a `dtype` discriminator, and legacy records keep reading as `Float32Array` (no migration). ### Removed - **`apps/showcase-nextjs`** — the 34-app Next.js demo showcase, retired and removed from the pnpm workspace. Its user-facing capabilities were absorbed at parity into the `/blocks` gallery, `localmode.ai` now serves the registry + gallery in its place, and all 34 legacy `localmode.ai/` URLs permanently redirect to their successor blocks. The source remains in git history and can be restored from the last commit that contained it (`git checkout -- apps/showcase-nextjs`). - **`@localmode/devtools/widget`** — the built-in DevTools widget UI. The data layer (collectors + bridge) and the `/react` hooks are unchanged; the successor UI is the `ui/devtools` registry family plus the composed `ui/blocks/devtools-drawer`. ### Breaking Changes - **`@localmode/devtools` 3.0.0** — `import { DevToolsWidget } from '@localmode/devtools/widget'` no longer resolves. The data layer and `/react` hooks are unchanged; migrate to the `ui/devtools` primitives or `ui/blocks/devtools-drawer`. ### Backward Compatibility - Runtime behavior is unchanged, but two `@localmode/core` **type** signatures moved (hence the 2.3.0 minor, not a major): `StorageAdapter.getVector()`/`getAllVectors()` now return `Float32Array | Uint8Array` (callers assigning straight to `Float32Array` must narrow), and `jsonSchema` lost its second type parameter (it never inferred). - Agent tool approval, the `useAgent` approval surface, `useEncryptedVault`, and the storage-adapter compressed-vector fixes are additive with no migration. `@localmode/ui` is a first release, so it has no prior install commands or routes to preserve — only the retired showcase app's URLs, which redirect. ## [@localmode/wllama@3.0.0] - 2026-05-28 ### Added - **Upgraded to wllama v3** (`@wllama/wllama@^3.2.3`) — migrated from v2's custom API to v3's OAI-compatible API (`createChatCompletion`, `createCompletion`, `createEmbedding`). The public `@localmode/wllama` API remains backward-compatible for existing consumers. - **Embedding models** — New `WllamaEmbeddingModel` class implementing `EmbeddingModel`. Factory method `wllama.embedding(modelId)`. 3 curated GGUF embedding models: nomic-embed-text-v1.5 (768d, 78MB), mxbai-embed-large-v1 (1024d, 197MB), bge-small-en-v1.5 (384d, 35MB). Dimensions auto-detected from GGUF metadata. New exports: `WllamaEmbeddingModel`, `WllamaEmbeddingSettings`. - **WebGPU acceleration** — `useWebGPU: boolean | 'auto'` and `nGpuLayers: number` settings. GPU offload with automatic WASM fallback. `gpuAccelerated` property on model instances. - **Tool calling** — `providerOptions.wllama.tools` and `tool_choice` forwarded to v3's OAI-compatible chat completion. Results include `toolCalls` array. 8 models verified: Qwen 2.5 (0.5B, 1.5B, Coder 1.5B, 3B, Coder 7B), Llama 3.2 (1B, 3B), Phi-4 Mini. - **Vision / multimodal** — `mmprojUrl` setting loads vision projection GGUF. `supportsVision` auto-detected. Base64 images converted to ArrayBuffer. Holo2 4B/8B catalog entries now include `mmprojUrl`. - **Jinja chat templates** — Enabled by default. v3's template engine handles chat formatting. Graceful fallback on template errors. Opt-out with `useJinja: false`. - **Model catalog** expanded from 18 to 30 models (25 language + 3 embedding + 2 reranker). New `WllamaModelEntry` fields: `supportsToolCalling`, `isEmbeddingModel`, `isRerankerModel`, `dimensions`, `mmprojUrl`, `nGpuLayers`. - **Gemma 4 GGUF models** — `Gemma-4-E2B-IT-Q4_K_M` (3.46GB, 131K context, 5.1B params / 2.3B effective PLE) and `Gemma-4-E4B-IT-Q4_K_M` (5.41GB, 131K context, 8B params / ~4B effective PLE). Vision + tool calling. Uses bartowski for main GGUF, ggml-org Q8_0 for mmproj vision projector files. - **New catalog models** — Qwen3 (0.6B, 1.7B, 4B), DeepSeek R1 Distill (1.5B, 7B), 2 reranker models (jina-reranker-v2-base-multilingual, bge-reranker-v2-m3). - **True streaming** — `doStream()` now uses `stream: true` in `createChatCompletion()` for real token-by-token streaming instead of buffered output. - **Structured output / JSON mode** — `response_format: { type: 'json_object' }` support via `responseFormat` option. Grammar-based JSON constraint via `providerOptions.wllama.grammar` (GBNF). - **Reranking** — New `WllamaRerankerModel` class implementing `RerankerModel`. Factory method `wllama.reranker(modelId)`. 2 curated reranker models in catalog. - **Reasoning mode** — `reasoning: boolean`, `reasoningFormat`, and `reasoningBudgetTokens` settings for models with thinking/chain-of-thought capability (e.g., Qwen3, DeepSeek R1). - **Performance config** — `cacheTypeK`, `cacheTypeV` (KV cache quantization), `flashAttention`, and speculative decoding settings for advanced performance tuning. - **Grammar sampling** — GBNF grammar support via `providerOptions.wllama.grammar` for constrained output generation. - **Model management** — `listCachedModels()`, `clearAllModelCache()`, `refreshModel()` for managing downloaded GGUF models in browser storage. - **LoRA adapters** — Support for loading LoRA adapter files alongside base models. - **Extended sampling params** — `min_p`, `seed`, and additional sampling parameters via provider options. - **Audio input (experimental)** — `AudioPart` content support for models with audio capabilities. - **Showcase app updates** — llm-chat: "Tools" and "Vision" capability badges, embedding models filtered from chat list, WebGPU passthrough, `providerOptions` threading through `useChat` hook. gguf-explorer: "Capabilities" inspect section with badges, new embedding model cards, JSON mode toggle. ### Changed - Single WASM binary (v3.2.3) replaces dual single-thread/multi-thread binaries. CDN URL: `@wllama/wllama@3.2.3/src/wasm/wllama.wasm`. - Streaming uses v3's `createChatCompletion({ stream: true })` AsyncIterable instead of the v2 `onNewToken` callback-to-queue bridge. - Token usage from OAI response `usage` field instead of `tokenize()`. - Stop sequences as strings via `stop` field instead of token ID lookup. ### Removed - `outputTokenIds` runtime extension (v3 OAI API does not expose per-token IDs). - v2 internal APIs: `tokenize()`, `samplingInit()`, `lookupToken()`. ## [@localmode/transformers@4.0.0] - 2026-05-24 ### Added - **Gemma 4 ONNX models** — Added Gemma 4 E2B and E4B to the LLM catalog (16 total, up from 14). New `isGemma4Model()` detection routes Gemma 4 through the VLM loading path with `Gemma4ForConditionalGeneration`. - **Gemma 4 vision support** — Gemma 4 models are vision-capable, bringing the total to 5 vision-capable ONNX models (up from 3). - **New blog post** — Comparison article: Gemma 4 LiteRT vs ONNX. - **`vad()` factory method** — `transformers.vad(modelId)` creates a `VADProvider` for use with `createLiveTranscriber()`. Backed by the Silero ONNX model. - **Generative OCR** — `transformers.ocr()` now auto-detects and routes GLM-OCR and LightOnOCR-2 models to a vision-language OCR path using `AutoModelForImageTextToText`. Two new catalog entries: `GLM_OCR` and `LIGHTONOCR_2_1B`. - **Kokoro TTS integration** — When a Kokoro model ID is requested via `transformers.textToSpeech()`, synthesis now routes to a dedicated phonemizer-backed path using `StyleTextToSpeech2Model` from transformers v4 + the `phonemizer` npm package (eSpeak-NG WASM). Dramatically better pronunciation compared to the generic pipeline. - **29 named English voices** — American English (21) and British English (8). Exposed via `TextToSpeechModel.voices` field and `DoSynthesizeOptions.voice` parameter. - **Voice catalog export** — `KOKORO_VOICES` constant with metadata (id, name, language, languageLabel, gender), `KOKORO_DEFAULT_VOICE`, `KOKORO_LANG_MAP`, and `KokoroVoice` type — all exported from `@localmode/transformers`. - **Speed control** — `speed` parameter (0.5–2.0) now forwarded to Kokoro synthesis. - **Provider options** — `providerOptions.kokoro.dtype` for quantization control (q8/fp16/fp32/q4/q4f16, default q8). - **New dependency** — `phonemizer` (^1.2.0) added (eSpeak-NG WASM for text→phoneme conversion). Note: `kokoro-js` was NOT used due to v3/v4 version conflict — Kokoro synthesis reimplemented directly using transformers v4. - **New showcase app** — `voice-studio` — browse all 29 English voices, streaming synthesis with speed control, side-by-side voice comparison. ### Breaking Changes - **Unified Transformers.js dependency** — Migrated from `@huggingface/transformers@^3.8.1` to `@huggingface/transformers@^4.2.0`. The npm alias `@huggingface/transformers-v4` has been removed entirely. All 26 implementation files now import from a single `@huggingface/transformers` package. ### Changed - **Audiobook Creator upgraded** — Switched from MMS-TTS (`Xenova/mms-tts-eng`, 30MB) to Kokoro TTS (86MB). Added voice selector dropdown (29 English voices), speed slider (0.5–2.0x), streaming playback via `useStreamSpeech`. - Kokoro model registry entry updated: 29 English voices, phonemizer-backed, speed control. - All pipeline-based implementations now pass explicit `dtype: 'fp32'` instead of `undefined` when quantization is disabled, eliminating "dtype not specified" log noise. - Removed `embedding-v4.ts` experimental benchmark file (no longer needed with unified v4). - Cleaned up `utils.ts` conditional v3/v4 import branching. - Removed "experimental" / "preview" labels from language model types, provider, and model catalog. ### Backward Compatibility - Non-Kokoro TTS models (SpeechT5, MMS-TTS) continue using the generic pipeline unchanged. - All existing `synthesizeSpeech()` and `streamSynthesizeSpeech()` calls work as before. - The public API is unchanged for the v3→v4 migration. If you imported `TransformersV4EmbeddingModel` or `createV4EmbeddingModel`, use `TransformersEmbeddingModel` / `createEmbeddingModel` instead. Re-test model outputs — embedding cosine similarity is ≥0.9999 and classification labels/scores are identical in validation testing. ### Fixed - **ImageTextToText tokenizer crash** — `loadImageTextToText` (GLM-OCR, LightOnOCR-2) now loads an `AutoTokenizer` alongside the processor, fixing a `TypeError` when calling `generateText()` or `streamText()` with a text-only prompt (no images). - **Kokoro TTS unrecoverable load failure** — If the Kokoro model fails to download (transient network error), the module-level promise is now cleared so subsequent calls can retry instead of permanently returning the cached rejection. ## [@localmode/core@2.2.0] - 2026-05-24 ### Added - **Audit Log** (`core/src/security/`) — Append-only, hash-chained, cryptographically signed, and optionally encrypted audit log for local-first compliance use cases. New exports: `createAuditLog`, `verifyChain`, `exportAuditLog`, `deriveAuditKey`, `generateEphemeralAuditKey`, and `AuditLogError`. Supports key derivation (PBKDF2) via `deriveAuditKey` and ephemeral session keys via `generateEphemeralAuditKey`. Chain integrity verified with `verifyChain`; full log export via `exportAuditLog`. All operations are offline and use the Web Crypto API — no external dependencies. React hook `useAuditLog` added to `@localmode/react`. - **Live Transcription** (`core/src/audio/`) — Streaming speech-to-text with voice-activity detection (VAD) and a turn-taking orchestrator for real-time conversational AI. New factory exports: `createLiveTranscriber`, `createTurnTaker`. Built-in VAD providers: `EnergyVADProvider` (threshold-based, zero-latency) and `SileroVADProvider` (neural VAD via Silero ONNX model). AudioWorklet helpers: `registerEnergyVADWorklet`, `createScriptProcessorVADNode` (fallback for browsers without AudioWorklet). Capability detection: `isLiveTranscribeSupported`, `isAudioWorkletSupported`, `isMediaCaptureSupported`. Error: `MediaNotSupportedError` (thrown when `getUserMedia` or AudioContext is unavailable). React hooks `useLiveTranscribe` and `useTurnTaker` added to `@localmode/react`. - **Silero VAD implementation** in `@localmode/transformers` (`silero-vad.ts`) — `TransformersSileroVAD`, `createSileroVAD` factory, and `SileroVADSettings` type. Provides a neural VAD provider backed by the Silero ONNX model via `@huggingface/transformers` for high-accuracy speech boundary detection. - **Streaming Speech** (`core/src/audio/`) — `streamSynthesizeSpeech`, `playStreamedSpeech`, and `splitIntoClauses` (with `DEFAULT_ABBREVIATIONS`) for clause-by-clause streaming TTS playback. React hook `useStreamSpeech` in `@localmode/react`. - **Generative OCR `prompt` parameter** — `ExtractTextOptions` and `DoOCROptions` now accept an optional `prompt` for table/formula recognition with generative OCR models. - **Capability detection** — New `isAudioWorkletSupported()`, `isMediaCaptureSupported()`, and `isLiveTranscribeSupported()` functions. New `LiveTranscribeCapability` type added to `CapabilityReport`. - **`MediaNotSupportedError`** — New error class thrown when `getUserMedia` or AudioContext is unavailable. - **`useExtractText` prompt support** — React hook now accepts a `prompt` option for generative OCR models. - **`AudioPart` content type** — Added to `ContentPart` discriminated union in `packages/core/src/generation/types.ts` — `{ type: 'audio', data: string (base64), mimeType: string }`. Backward-compatible additive change; existing `TextPart | ImagePart` consumers continue to work unchanged via the `type` discriminator. ## [@localmode/mediapipe@2.0.0] - 2026-05-24 ### Added - **New provider package**: `@localmode/mediapipe` wrapping Google's MediaPipe Tasks — `@mediapipe/tasks-vision`, `@mediapipe/tasks-audio`, and `@mediapipe/tasks-text` — as a single unified provider. WASM + WebGL runtime, works in all target browsers (no WebGPU required). - **New core interfaces** for landmark and gesture tasks: `HandLandmarkModel`, `PoseLandmarkModel`, `FaceDetectionModel`, `FaceLandmarkModel`, `GestureRecognitionModel` in `packages/core/src/vision/`, and `LanguageDetectionModel` in `packages/core/src/translation/` — all interface-only, zero new core dependencies. - **New core functions**: `detectHands()`, `detectPose()`, `detectFace()`, `detectFaceLandmarks()`, `recognizeGesture()` (vision) and `detectLanguage()` (text). - **New core constants**: `HAND_CONNECTIONS`, `POSE_CONNECTIONS`, `FACE_CONNECTIONS` (landmark topology for drawing overlays), `GESTURE_CATEGORIES` (8 standard gestures), `SUPPORTED_LANGUAGES` (ISO 639-1 code → name map). - **MediaPipe model implementations** for new interfaces (hand/pose/face landmarks, face detection, gesture recognition) and existing core interfaces — `ImageClassificationModel`, `ObjectDetectionModel`, `SegmentationModel`, `ImageFeatureModel` (vision), `AudioClassificationModel` (YAMNet, 521 categories), `ClassificationModel` and `EmbeddingModel` (text), `LanguageDetectionModel` (110 languages). - **Provider-specific streaming API** — `createHandTracker()`, `createPoseTracker()`, `createFaceTracker()`, `createGestureTracker()` run MediaPipe vision tasks in VIDEO mode over a `