Docs / project

Changelog

Released versions, what improved, and what shipped with a known gap. Versions match the git tags.

The releases page renders the same data, with desktop installers and checksums available on the download page.

Known gaps are listed deliberately. The people most likely to read a changelog are deciding whether to trust the thing, and a changelog with no bad news is marketing.


v3.1.0 — 2026-11-02

Research only the files you choose, with every claim checked against them before you see the report. The desktop app is published for Apple Silicon Macs only from this release.

Added

  • Choose files to research (#212–#217, #227–#229). Tick files and folders on the Corpus page, then Ask about these files. The run may read only those files, at the versions they had when it started. The selection is frozen on the run (research_runs.corpus_selection), enforced in the store (ScopedCorpus, not a prompt), and recorded in the bundle's corpus_selection trace entry. The 3.1 verifier checks every corpus source against it, and an older verifier still passes the bundle.
  • Every claim checked before you see it (#221, #222, #240, #260–#262, #272). In research limited to your corpus, each sentence is split into claims. Each claim is checked against the passages its citations point to: figures must appear there, a calculation is redone by the code, an irrelevant citation is removed, and a conclusion must follow from its findings. A failing passage is rewritten from what is supported and checked again, or removed with a note. A run whose claims could not all be checked ends without a report.
  • The checker's measured accuracy is shown (#273, #277). The run page and the exports say whether the checking model met the G-6 threshold, fell below it, or was never measured.
  • Folders (#227, #228, #263, #268). Upload a folder with its subfolders, or drop one onto the Corpus page. Rename, move and delete folders; the delete confirmation says how many files go. Stop an upload part-way.
  • Many more file kinds (#223–#226, #245). Word, OpenDocument, RTF, spreadsheets, PowerPoint, EPUB, notebooks, LaTeX, BibTeX and source code. A citation names its page, sheet and row, slide, cell or line, and opens with the passage highlighted; a PDF opens at the cited page (#230, #242, #246).
  • Offline (#219). Research refuses to start unless every model runs at an address on this computer.
  • A corpus per project on the desktop (#220, #243, #247, #253). Documents from before 3.1 move to "Documents from before 3.1", and the original store is kept.

Fixed

  • A Custom Endpoint with no address is refused (#274). It was sent to api.openai.com with the Custom Endpoint key and the prompts. The desktop also never read CUSTOM_BASE_URL from the environment; both hosts now read keys and the endpoint through local.provider_keys_from_env.
  • Dropping files or folders onto the desktop app works (#268). Tauri's native drag-drop handler took every drop before the page saw it.
  • The desktop runs under the server's run limits, and Settings sees a saved Custom Endpoint (#237).

Measured

  • G-6, the grounding benchmark: 425 claims labelled by an independent model judge (Claude Sonnet 4.5). On the default routing (custom:auto/best-fast): false accept 1.9% (Wilson upper 4.3%), false reject 5.1%, unmeasured 4.5%, which meets the threshold. On custom:agy/gemini-3-flash: false accept 2.6% (upper 5.2%), which is below it and labelled so.
  • G-5, the checking cost on the default routing: a median 115 s instead of 33 s, p95 249 s instead of 80 s (3.13×), and checking adds a median 152% to input tokens. That meets the thresholds set in amendment A-7.
  • G-4: bundles from real selected-files runs verify under the unchanged v3.0.2 verifier.

Known

  • The desktop app is published for Apple Silicon Macs only. Windows and Linux users keep v3.0.3, which has none of this release's corpus features.
  • Checking every claim makes research on your files slower and costlier (see Measured). Below about 120,000 input tokens, MAX_INPUT_TOKENS can stop the check part-way, and the run then ends without a report.
  • The checker's accuracy was measured against a model judge, not a person, and for two routings only. Every other model is labelled "not measured".
  • Offline checks each model's address. A Custom Endpoint on this computer that forwards to a cloud provider passes the check while your text leaves the machine.
  • Research on the web is not checked claim by claim.
  • A subfolder the app may not read is left out of a folder upload without a message. Scanned PDFs need text recognition first.
  • MAX_COST_PER_SESSION_USD does nothing on openrouter and custom routes, whose cost is recorded as $0.00 whatever a run spent.
  • Every other v3.0.3 known gap still applies, except that projects no longer share one corpus on the desktop, and corpus-only mode's model locality now has the Offline switch.

v3.0.3 — 2026-10-26

The desktop app researches on a local model with no API key, as Settings always said it could.

Fixed

  • Desktop research on a local model needs no API key (#202). sidecar_run_config refused any run with no key stored, whatever the routing, while the CLI and the server exempt ollama and custom. The rule is now research_engine.local.providers_missing_a_key, which the CLI and the desktop both call, so the two cannot disagree about what "no key" means. Present in every desktop release since 2.1.0.
  • The desktop offers the local providers it can reach (#202). Its catalog answered available_providers from stored keys alone, so ollama was never listed and Settings → Models offered no local preset. local.usable_providers lists ollama unconditionally, as model_routing.available_providers does on the server, and custom once its base URL is saved.
  • Saving a Custom Endpoint base URL works on the desktop (#202). PUT /desktop/keys/custom_endpoint was declared after PUT /desktop/keys/{provider}, which matched first and answered "Unknown provider."; the endpoint routes are now declared first. test_host_parity passed throughout, because the route was registered and nothing called it.
  • The run form's Local LLM default skips chat-only models (#202) while a larger one is installed.
  • The per-account sign-in limit counts failures only (#200), as docs/06 §4 states. auth.login raised both counters before checking the password, so a sixth correct sign-in within 15 minutes was refused with 429. The account counter is now read with a non-counting rate_limit.peek and raised only on a failure — an unknown address exactly like a wrong password, so the lock does not reveal which addresses have accounts. Five failures still lock the account, the right password included; the per-IP limit still counts every attempt.
  • Settings → Appearance hydrates cleanly (#201). The theme radios rendered their selection from next-themes' resolvedTheme, which is undefined on the server, so every full load threw React error 418. The picker shows no selection until mount, through a shared useMounted hook that ThemeToggle and RelativeTime now use too; RelativeTime's hover text also waits for mount, since the server renders it in UTC.
  • The public site fetches nothing (#205). ActiveProjectProvider sat in the root providers, so from 2.0.1 on every page of the GitHub Pages site requested /api/v1/projects and logged a 404. The (app) layout provides it now.
  • Corpus-only mode is described as what it enforces. The comparison table, the landing page, the README, the user guides, the local-model guide, the architecture docs, the request schema and the engine's docstrings and error message promised a corpus-only run sent nothing over the network, and the login page called local indexing "offline". The code enforces three things: evidence only from the installed corpus; web search off, with read_webpage refusing every non-corpus URL; and indexing on a local model (corpus.py::_require_local_embedder_in_corpus_mode). It does not check the fourth — where the planner, executor, critic and synthesizer send the question and excerpts — and test_corpus_egress.py runs with scripted models, so it proves the retrieval half only. Every surface now states the four separately, and none presents local indexing as running offline. tests/workflow/test_corpus_privacy_copy.py fails if either kind of claim returns. Chat's My corpus scope, whose copy is also the model's instruction, no longer says "only the documents uploaded to this project": on the desktop app every project shares one corpus.

Process

  • vite 8.3.5 (#208) and undici 8.12.2 (pinned under jsdom with an overrides entry) in the frontend's test toolchain, each past a high-severity advisory. Both are development dependencies, in neither the server image nor the desktop app.
  • 3.0.2's flip, post-release audit and record updates (#203, #204, #206). The flip also corrected the download page's installed size to ~150 MB and reworded RELEASE.md's invalid-key check (M20.03) to describe the shipped behaviour.

Known

  • Corpus-only mode keeps the evidence local but does not stop model calls from leaving the machine: the question and retrieved excerpts go to whichever provider the planner, executor, critic and synthesizer are set to use — a cloud provider unless you have chosen local models. A local indexing model does not change that; corpus mode does not check.
  • On the desktop app every project shares one corpus (corpus.sqlite), so corpus-only research and chat's My corpus scope draw on documents uploaded under any project.
  • Research saved before 3.0.2 keeps its duplicated evidence rows after a rework; see 3.0.2 below.
  • MAX_COST_PER_SESSION_USD does nothing on openrouter and custom routes: validate_pricing skips them, so estimate_cost records $0.00 whatever a run spent. Cap spending at the provider.
  • Verifying a bundle with this release's verifier needs Python 3.14 or later; on 3.13 or earlier it refuses to start. The bundle format has not changed, so the verifier from 3.0.1 still verifies this release's bundles on an older Python.
  • Citation support was last measured for 3.0.2. Nothing that measurement covers has changed since (no prompt, graph, routing, retrieval, tool or claim code), so this release was not re-evaluated.
  • Everything listed under 3.0.1's Known below still applies.

v3.0.2 — 2026-10-19

Research through your own model router runs for real, and a reworked report's counts describe the report you approved.

Fixed

  • Custom, OpenRouter and Ollama setups research for real (#171, #177). start.sh decided between real and demo mode from Google, Anthropic and OpenAI keys alone, and on any other setup exported LLM_MODE=fake, which outranks .env under Compose, so every question got the scripted demo report. Its check now mirrors app/config.py::_validate_secrets: any keyed provider, or a MODEL_* route to a keyless one. tests/workflow/test_start_sh_mode.py runs the script's real decision block against fixture .env files.
  • Approving a plan no longer fails the run (#171, #177). langchain_openai and langchain_anthropic cache their async HTTP client per process, and every Celery task ran on a fresh event loop, so a plan-approval task landing in the same prefork child as the start task met pooled connections from a closed loop. The worker now keeps one loop per process (app/workers/event_loop.py), refuses a second thread, and closes the loop on worker_process_shutdown. The error users saw — SQLAlchemy's Method 'close()' can't be called here — was a second defect masking the first: executor_node's asyncio.gather left sibling tasks writing through the session the driver was closing. research_engine/concurrency.py::gather_or_cancel cancels and waits out the siblings and re-raises the earliest failure.
  • The provider SDKs are pinned (#171). openai 2.x → 3.x reached the server image with no change in this repository. backend/constraints.txt pins the provider SDKs, their LangChain integrations and the HTTP stack beneath them, and requirements.txt applies it with -c.
  • A non-string read_webpage url is an observation, not a crashed node (#171).
  • Search-engine results pages are refused (#178). read_webpage refuses a results page on Google, Bing, DuckDuckGo, Yahoo, Brave, Ecosia, Qwant, Mojeek, Baidu, Startpage or Yandex before any fetch, with an observation that names web_search (tools.is_search_results_page). Both tools' descriptions and the executor prompt's first step name web_search. The shipped executor prompt changed, so its SHA-256 in run bundles changes with it.
  • A rework no longer duplicates evidence (#181). Both hosts persist a run's whole final state again after a rework, and record_evidence inserted every item a second time — doubling the run's evidence, the revision's evidence_watermark and the bundle. Each incoming item is now paired one-for-one with a stored row (same source, task, snippet and key fact) and reuses it; record_contradictions pairs the same way.
  • Cited counts describe the latest revision (#181). Each revision in the run graph carries cited_indices, the markers its body cites with the reference list excluded (API). The run page's cited and uncited counts and "backs N claims" count against the latest revision; a source only an earlier draft cited is listed as retrieved, not cited. Source.citation_index is the run's numbering of every source the synthesizer was shown, not a statement that a report cites it (the research record, corrected).
  • An approval is not a 500 when filing into project memory fails (#180). _ingest_report_into_memory read run.id after its rollback had expired it, so a server with no embeddings provider answered 500 for an approval it had recorded.
  • A demo run says so first (#177). DemoNotice leads the run page and the session view in every state: the question was not sent to a model, and when every run is a demo the server itself is in demo mode.
  • Reset from inside the agent editor (#179). Settings → Agents' editor offers Reset to default when the agent is customized or has unsaved edits.
  • The server images are stamped (#170). release.yml stamps the api and worker images with the commit that built them and checks that each pushed image reports it. Through 3.0.1 every image answered /api/v1/version with unknown.
  • The Oracle Cloud deploy defaults to latest (#170). It defaulted to edge, a development build made on 2026-08-05, before 1.0.

Measured

  • The executor prompt changed (#178), so this release was evaluated before shipping: the ten fixed questions with every agent on custom:antigravity/gemini-2.5-flash and the citation judge on custom:kiro/claude-sonnet-4.5, the routing and judge 3.0.0 was measured on. All ten completed, and citation support measured 96.9% over 190 judged claims, none unjudged (backend/evals/results/eval-2026-10-19-custom.json). 3.0.0 measured 96.4% the same way; one run each is not evidence of a change in either direction.

Process

  • A mandatory release process (#170): RELEASE.md, scripts/release_audit.py and release-audit/. CI now installs the .deb, unpacks the AppImage and installs the .msi, and drives the engine inside each through a research journey; the site publishes /build.json.
  • The release audit judges the site a release commit actually serves. A3.02 used to fail a release commit that changed nothing the site is built from, because pages.yml is path-filtered and skips it; it now judges the Pages run of the last commit that did.
  • A packaged-app check whose bundle verification did not run fails CI (#194). check_packaged_sidecar.py --require-verify makes the standalone verifier's verdict on the journey's bundle part of the pass for the .deb, AppImage and .msi.
  • The server, the desktop engine and every CI job run Python 3.14 (#173, #187, #189), up from 3.11. #173 moved the server images alone, and nothing built them before a tag; CI and the release stamp job moved to match (#187), then the PyInstaller engine (#189). A10.06 fails any commit whose workflows and backend/Dockerfile name different Pythons.
  • Python 3.14 is the floor everywhere (#193). requires-python, ruff's target, an import guard in research_engine, the standalone bundle verifier and the release audit all say 3.14, and A10.06 fails a commit where any stated floor differs from the image.
  • Every build reports its own identity, or unknown (#188). A stale, git-ignored research_engine/_build.py in a checkout was copied into every local image, which then claimed another build's version and commit at /api/v1/version. The Dockerfile now discards a stamp in its context unless release.yml keeps its own; start.sh stamps local images from the checkout; the sidecar spec refuses a stamp that does not describe the checkout.
  • Security updates. Next.js 16.3.8 fixes a high-severity server-side request forgery advisory in Image Optimization, which the self-hosted standalone server leaves enabled, plus two medium and one low advisory (#185); sharp 0.35.5 (#184) and source-map-js 1.2.2 (#183) are past high-severity advisories of their own. postcss-selector-parser is patched to 7.1.6 under the typography plugin, which pins the vulnerable 6.0.10 even in its latest release (#199, a build-time dependency only).
  • Dependency updates: Next.js 16.3.6 and the frontend minor group, undici 8.11.2, brace-expansion 1.1.21 (#174, #175, #176, #182); then Next.js and eslint-config-next 16.4.2 and Playwright 1.64.1 (#191), and numpy>=2.5.4 (#190).
  • The frontend image, CI, the desktop and Pages builds all run Node 26 (#172, #196). #172 moved the image from 25 while every job that builds the frontend ran 22; #196 moved those jobs to 26 and made Node 26 the stated floor for developing the frontend (engines in frontend/package.json, CONTRIBUTING, the guides). A10.07 fails any commit whose workflows, frontend/Dockerfile or stated floors name different Nodes.

Known

  • Research saved before this release keeps its duplicated evidence rows after a rework. Its snippet count stays doubled, and a run approved before 3.0.2 keeps the duplicates in its frozen artifact and bundle; deleting them would make the live run disagree with what was approved. Cited counts and "backs N claims" are computed when the page loads, so they are correct on older runs too.
  • MAX_COST_PER_SESSION_USD does nothing on openrouter and custom routes: validate_pricing skips them, so estimate_cost records $0.00 whatever a run spent. Cap spending at the provider.
  • Verifying a bundle with this release's verifier needs Python 3.14 or later; on 3.13 or earlier it refuses to start. The bundle format has not changed, so the verifier from 3.0.1 still verifies 3.0.2 bundles on an older Python.
  • More than five successful sign-ins to one account within 15 minutes lock it for the rest of the window: the per-account limit counts every attempt, not only failures as docs/06 §4 states. Present since before 1.0; fixed on main for the next release (#200).
  • Every full load of Settings → Appearance logs React error 418 (a hydration mismatch) in the browser console; the page recovers and works. Present in 3.0.1 too; fixed on main for the next release (#201).
  • On the desktop app, research on a local model (Ollama) with no API key saved fails at once with "No provider key found": sidecar_run_config refused any run with no key stored, whatever the routing. Settings → Models offers no local preset, because the desktop catalog never listed ollama as available, and Save URL for a custom endpoint answers "Unknown provider.", because /desktop/keys/{provider} matched its path first. Present in earlier desktop releases too. Workaround: save any text as the Custom Endpoint key in Settings → Connections (Ollama ignores it); research then runs on the first model Ollama lists. Fixed on main for the next release (#202).
  • Everything listed under 3.0.1's Known below still applies.

v3.0.1 — 2026-09-28

The macOS app opens. Every Mac download before this one was reported as damaged.

Fixed

  • macOS builds are signed as a whole bundle. Every macOS build from 1.0.1 to 3.0.0 carried only the ad-hoc signature the linker stamps on an arm64 executable (flags=adhoc,linker-signed, Sealed Resources=none): nothing signed the .app, so codesign --verify failed with "code has no resources but signature indicates they must be present". A copy downloaded by a browser carries com.apple.quarantine, and Gatekeeper reports a quarantined app whose signature fails verification as "damaged and can't be opened" — no Open Anyway, only the Trash. bundle.macOS.signingIdentity: "-" in desktop/tauri.conf.json now has the bundler ad-hoc sign the binary and then the whole bundle, sealing the engine under Contents/Resources/sidecar/, before the disk image is built. It is still not a Developer ID signature and is not notarized.
  • CI refuses a disk image whose app is not validly signed. The macOS shell job mounts the .dmg it is about to publish and fails if the app inside is only linker-signed, fails codesign --verify --deep --strict, or — as a quarantined copy — gets anything from spctl other than a policy verdict. Run against the shipped 3.0.0 image (fails: linker-signed), a correctly signed image (passes) and one edited after signing (fails: a sealed resource is invalid).
  • Install steps that work. The download page, the release notes and the desktop guide sent Mac users to Open Anyway, which the damaged builds never offered, and to right-click → Open, which current macOS no longer offers. They now give the command that opens every macOS build, older versions included: xattr -dr com.apple.quarantine "/Applications/Research Assistant.app".

Research is unchanged from 3.0.0.

Known

  • Desktop builds are still not signed with a publisher certificate or notarized, so macOS blocks the first launch until the download flag is cleared, and Windows shows SmartScreen.
  • Everything listed under 3.0.0's Known below still applies, apart from its description of the macOS warning, which this release corrects.

v3.0.0 — 2026-09-28

You can rewrite how each research agent behaves, and every artifact now records exactly which instructions produced it.

Added

  • Custom agent instructions. Each of the five public roles — planner, executor, critic, synthesizer and follow-up chat — can run on a user's own instructions instead of the shipped prompt. Settings → Agents edits them one role at a time, starting from the prompt that runs today, and resets any role independently. The limit is 2,500 characters per role; an empty or whitespace-only body is rejected rather than read as a reset (null is the reset).
  • Protected purposes. Five roles fan out across nine prompts, and eligibility is per prompt, not per role. Citation verification, contradiction detection, report repair and the project-chat refusal line always compose the shipped prompt, decided in one function (prompt_composition::system_prompt) before any override is read.
  • A run keeps the instructions it started with. Overrides are resolved once, at start, into research_runs.effective_prompt_overrides, so an edit made while a run waits at a review gate applies to the next run and never to the half-written one.
  • Bundle format v2. Every run's bundle records each prompt the pipeline composed: its full text, its SHA-256 and whether it was replaced, plus the run's prompt_overrides_status (NONE, APPLIED or UNUSABLE). The provenance is inside the bundle hash, so editing it breaks bundle_integrity. The standalone verifier accepts v1 and v2 and prints the provenance beside its verdict.
  • Disclosure in the app. A run's artifact panel names the roles that ran on replaced instructions, and research recorded on the earlier pipeline says when it ignored instructions it was configured with.
  • Custom-spec evaluation. python -m evals.harness --candidate spec.json runs the fixed query set under the shipped prompts and under the candidate, against the same thresholds, and reports both.

Changed

  • The citation judge is named, independent and disclosed. The harness graded citation support with get_llm("critic") — the pipeline's own critic — and recorded no judge. A real-mode report eval now requires --judge provider:model and refuses, before any query runs, a judge that is a pipeline route, the same model under another provider or spelling, or a router alias. The result carries a judge block beside models, and each claim verdict records the model the provider says answered. The release run, backend/evals/results/eval-2026-09-28-custom-custom.json, is the first result graded this way: every pipeline role on custom:antigravity/gemini-2.5-flash, judged by custom:kiro/claude-sonnet-4.5, and every ruling records who served it (kr / claude-sonnet-4.5 / single). Under the shipped prompts it completed 10 of 10 and measured citation support at 0.9638, clearing both thresholds. The planner-only candidate on the same run completed 8 of 10 and measured 0.9389 — below both; its two incomplete queries were provider outages, recorded as unmeasured.
  • A release tag must name VERSION. scripts/sync_version.py --tag runs in both release workflows before anything is built, so a tag pushed ahead of its version bump fails instead of publishing mislabelled installers and images.
  • A refused preference gets the same 422 body on both hosts. The desktop validated the preferences object on its own, so a refused override came back located at ["prompt_overrides"] without the rejected input; it now raises the server's own validation error, located from body, and the two bodies are identical (scope freeze §7), pinned by test_prompt_override_422_parity.py. The message the UI shows is unchanged.
  • The packaged app is checked for what V3 ships. The desktop workflow's frozen-app smoke now asserts the bundle is v2 with provenance, that Settings → Agents' route answers, and that a saved override reaches the next run's bundle as APPLIED.
  • The v2.1.0 → V3 desktop upgrade is tested against a real v2.1.0 install. A fixture written by the v2.1.0 sidecar itself — preferences, a project, research on both pipelines, a run waiting at a gate and its exported bundles — is upgraded on every CI run, and scripts/check_packaged_upgrade.py repeats the journey against a packaged build.

Fixed

  • A stop now holds wherever it lands (issue #54). Three gaps in that guarantee predate 3.0.0 and were found when main's CI lost a cancellation race after the release branch merged:

    • On the server, a session stopped mid-run reached the review gate anyway. A run stopped mid-run kept its status only because ck_run_cancelled rejected the late write, which also discarded its spend and crashed the worker. Both workers held the object they loaded at start, so their guards never saw the stop. They now re-read it from the row.
    • On both hosts, a session stopped before its driver started was set back to RUNNING and stayed there. The start write is now app/session_lifecycle.py::mark_running, conditional on the stop inside the UPDATE.

    Each is pinned by a deterministic test in test_cancellation_is_authoritative.py that fails with its guard removed.

Known

  • Bundles from this release are format v2, and a verifier from before 3.0.0 refuses them. The current verifier still verifies every v1 bundle unchanged.
  • Research recorded before 3.0.0 re-exports as v1: its prompts were never captured, and the assembler does not invent them.
  • Sessions (the earlier pipeline) do not apply custom instructions; they record and show that they ignored them.
  • Customization is per account. There is no per-run override and no user-defined agent.
  • Custom instructions are written into the bundle in full, so anyone a bundle is shared with can read them.
  • The default routing is Google's Gemini 2.5 models — gemini-2.5-pro for the planner and synthesizer, gemini-2.5-flash for the executor, critic and chat — and Google now limits each 2.5 model to accounts that have used it before (its deprecations page, read 2026-09-27). On a key from an account that has not, the first call to that model is refused — the maintainer's key got 404 "no longer available to new users" from gemini-2.5-pro while gemini-2.5-flash still answered. Choose other models in Settings → Models.
  • On a fresh desktop install the first-launch demo fails: sessions.corpus_mode is created with the text default 'false', which reads back as true on SQLite, so the demo runs as corpus-only against an empty corpus. Present in 2.1.0 as well; user research is unaffected.
  • Citation support rests on one run: ten fixed questions on one model routing, judged by a model rather than a person. It measures whether a claim matches the evidence it cites, not whether the claim is true. Cost was not measured: custom: routes carry no price.
  • The share of evidence from primary sources is not measured, and deliberately not estimated.
  • Retrieval is dense-only. Desktop builds are unsigned and do not auto-update. Two research pipelines still exist in the backend. Report-scoped follow-up chat exists only on sessions. Cancelling a run does not interrupt work in flight. Claim verification and claim lineage are not implemented.

v2.1.0 — 2026-09-19

Two claims the product makes about itself turned out not to hold, and the things that would have caught them did not exist.

Fixed

  • Airgapped corpus mode did not hold on the server. A run requested as corpus-only, and recorded as corpus-only, could still search the open web and read pages from it. The flag reached the database and decided which corpus store to install, but never reached RunConfig — and retrievers.search and tools.read_webpage, which decide whether anything asks the web at all, read it from there and cannot see a database. So the guarantee was recorded rather than enforced, on the host that advertises it loudest. The desktop had been fixed earlier; the server had not. The egress test stayed green throughout because it constructed RunConfig by hand, stubbing the exact hop that was broken; it now drives the real host builders and asserts every retrieved source is a corpus:// location.
  • Both chat surfaces read the oldest twenty turns, not the newest. ORDER BY created_at ASC LIMIT 20 returns the first twenty messages ever written. Every caller commits the user's new message before reading that window and assembles its prompt only from it, so past turn twenty the model was answering a question it had never been shown. Three homes, not two — the sidecar restated the same query — so it became one function, chat_history.recent_turns, which selects newest-first and restores chronological order. Both halves are tested independently: fixing only the limit would leave the transcript reversed, which reads as a different conversation.
  • A migration could not be reversed. No downgrade had ever been run against this schema, and the first one attempted failed on every database: 0008_chat_threads dropped a constraint under a name the naming convention had already rendered differently, so every downgrade past that revision died. CI now runs a populated round-trip in both directions, with two exception registries that each name a reason.
  • Run identifiers were logged as session identifiers. session_id means a sessions.id; binding a run's id to that field made a run's own logs unfindable by its id. There are two binders now, and the distinction is enforced rather than remembered.
  • Scripted runs could pass by producing nothing. Fake mode selected its behaviour by searching the system prompt for eight hand-typed fragments of its own prose, with an else returning an empty object. Nothing tied those fragments to the prompts they quoted, so rewording any prompt would have routed every scripted test to that branch and left the suite green on a pipeline that produced nothing. Behaviour is selected by agent role now, and an unrecognised scenario raises instead of answering emptily.

Improved

  • Retrieval quality is measured. Four documents call retrieval the ceiling on report quality and a repository-wide search for recall@ or ndcg returned nothing at all. There is now a frozen sixteen-document dataset, recall@k / precision@k / nDCG / duplicate metrics against the real retriever, and a write-once baseline: recall@1 0.792 · precision@1 0.917 · nDCG@10 0.944 · dedup_rate 0.000.
  • A corpus has an identity, and its documents have versions. Identity used to be the file path, so a bundle citing corpus://<id> could say which bytes but never which corpus or what state it was in. Documents carry doc_key, version and superseded_at, a re-upload supersedes rather than duplicates, and a run records the corpus snapshot it read.
  • Operational metrics. A server-only metrics endpoint exposes seven metric families in Prometheus text on a private registry, with every label drawn from a closed table. Terminal outcomes are observed where runs actually terminate — completion at the approval route, failure and cancellation at the persistence adapter — and always after the commit.
  • A typed view of a report. ReportDocument is derived from the report Markdown and never the other way round, recording whether the parse was exact or lossy so a lossy one can never become authoritative. Markdown stays the source of truth: report_hash is still sha256(report_markdown), so every existing approval chain and bundle pins exactly what it pinned before.
  • Readiness tests are hermetic. Whether the settings page reports you as ready to research was asserted from a live probe of whatever machine ran the suite, so one commit passed or failed depending on whether Ollama happened to be running. Both hosts now stub the transport rather than the probe, and the reachable, unreachable and embedding-only cases are each pinned — including that a reachable server offering only embedding models is not readiness, since an embedding model cannot fill an agent role.

Measured and rejected

  • Hybrid retrieval did not beat dense-only. It was implemented and measured against the baseline above. The diagnosis is that the residual gap is equal-weight fusion of two retrievers of unequal reliability, not the quality of the candidates. It was rejected and reported rather than tuned until the number improved. Dense-only remains production behaviour.

Known

  • Citation support is still measured at 0.90 on a single self-judged local-model run, and that measurement predates 2.0.0. Nothing here re-ran it.
  • The share of evidence from primary sources is not measured, and deliberately not estimated: source_url is model-authored, so an invented but plausible link would classify as primary with full confidence. A fakeable metric on a verifiability product is worse than no metric.
  • The scholarly evaluation set is drafted but unverified. Six of twelve questions survived review, and their rubrics were written without a human opening a cited paper. A rubric naming the wrong study would penalize a correct answer, so the set grades nothing until a domain reader checks it.
    • Corrected 2026-09-27, in 3.0.0: this overstated it. The repository's own records (backend/evals/queries_scholarly_rubric.json) show three of the six surviving questions carry citation-check records dated 2026-08-16 — two verified, one verified_with_caveat — each stating that the works it names were looked up against the published record. The other three are unverified. The records do not say who performed the checks.
  • Desktop builds are unsigned and do not auto-update.
  • Two research pipelines still exist in the backend. The product has one, and research recorded by the earlier one stays readable; consolidating the two is not a patch.
  • Follow-up chat scoped to a single report is available on research recorded as a session and not on a run. Project chat, which cites every approved report in a project, covers both.
  • Cancelling a run still does not interrupt work already in flight; it runs to its next checkpoint, and the tokens spent there are recorded because they were really spent.
  • Claim verification is still not implemented, claim lineage across revisions is still not tracked, and contradiction detection is still source-level and unscored.

v2.0.2 — 2026-08-31

The desktop app stopped reimplementing the server, and four bugs that only existed because it had.

Fixed

  • Every desktop settings-page load 404'd in the background. useReadiness() is fetched unconditionally on every host — the settings layout only branches on what it does with the answer — and the sidecar had no route for it at all. A shipped control that never worked. It answers from this host's own keys now.
  • The "Local" model preset offered models the machine did not have. The server has built that preset from what Ollama actually reports since 2.0.0; the desktop kept the static name, which 404s on the first planner call if it was never pulled.
  • The routing panel's "deployment default" mirrored your own saved preference. Both numbers were computed from the same call, so saving a per-role choice made them identical — which defeats the reason the comparison exists.
  • A project with runs and no chat sessions reported zero sessions. The desktop's project list only counted the older kind of research.
  • Stopping a run recorded no reason in its event history, though the reason was visible elsewhere on the same screen.

Improved

  • The desktop and the server run the same code for every project, corpus and research-session operation that does not depend on where a secret is stored — proved by the two resolving to one function object, not by two implementations that currently agree.

Known

  • Two research pipelines still exist in the backend.
  • Follow-up chat scoped to a single report is available on a session and not on a run.
  • Cancelling a run does not interrupt work already in flight.
  • Claim verification is not implemented, claim lineage is not tracked, and contradiction detection is source-level and unscored.
  • Corpus-mode research has no end-to-end test, because it requires a local embedder and the test environment has none.
  • Citation support is measured at 0.90 on a single self-judged local-model run predating 2.0.0.

v2.0.1 — 2026-08-26

Four measurements that were wrong, a feature that was inert, and one product instead of two.

Fixed

  • Project memory could not index anything the current pipeline produced. Memory was keyed by a foreign key to the session table, so a research run counted as an approved report and could never be indexed: the "not indexed yet" count only climbed, and project chat answered from an empty store. Nothing failed and no test was red — the feature was simply inert for every account that had only ever used runs, which is every account created since 2.0.0. Both kinds of approved report now share one memory, and a citation in a chat answer links to whichever surface can open it.
  • Retrieval silently returned nothing. The relevance ceiling had been tightened from "worse than orthogonal" to a smaller unmeasured number, on the reasoning that it would filter noise. It filtered the answer: a question asked of the project that owns the report retrieved zero results. Restored, with a test that states the rule rather than leaving three unrelated tests to imply it.
  • A run whose evidence was never read could still export a bundle. That refusal was attached to the import ledger, so it only covered imported runs; a run executed here whose checkpoint could not be decoded produced a bundle that numbered every [n] against nothing and asserted a quality nobody observed. The fact now lives on the run itself and one rule covers every run.
  • No approved report could ever be indexed, because the chunker discarded every section. Markdown does not require a blank line after a heading and this product's own synthesizer does not emit one, so ## Summary and the prose beneath it arrived as one block that the splitter treated as a heading — keeping the first line and dropping the paragraph. Every section took that branch, and a 1,600-character report chunked to nothing. Nothing raised: ingestion logged "report produced no chunks" and moved on. This is what made project memory unreachable in practice even where it was wired up.
  • The memory card counted the corpus as memory. indexed_reports and chunk_count were max(memory, corpus), so a project with uploaded documents reported reports as indexed that retrieval could not reach. Two stores, two cards, two numbers.
  • The verified-citation rate was never measured on a research run. The engine measures it on its own completed outcome and a run never reaches one — approving at the report gate finalizes the run in the domain rather than resuming the graph — so the number was NULL on every run ever produced, while History offered a filter on it and every run card displayed it. It is now measured at approval, from the exact bytes the reviewer approved.
  • The standalone verifier crashed on Windows. Every check passed and then printing the result raised UnicodeEncodeError, because a Windows console is cp1252 and ✓ is not in cp1252 — a traceback where the word PASS should have been. This is the one program here a stranger runs on their own machine to check an artifact they were handed, so it now falls back to ASCII markers on a console that cannot render the glyphs. Found by running the packaged desktop app on a Windows runner, which nothing had done before.
  • A forced extraction pass billed its first attempt twice. When a model declined to submit evidence and the fallback mechanism ran, the budget guard was handed the running total rather than the increment — so a spend limit could fire on money that was never charged.
  • The desktop app can start research. It drives a run in-process against its own checkpointer, because it has no broker to hand one to. In 2.0.0 the same button answered 501. Everything downstream is the server's code: the same engine, the same domain tables, the same artifact and the same bundle.
  • Approved reports are saved into their project's corpus automatically, so follow-up questions can draw on them without a re-upload.
  • A task that fetched pages but never submitted evidence no longer loses them. The engine asks for the extraction directly, from the text it already holds and nothing else, and retries through a differently-shaped request when a model ignores a forced tool call. Every quotation is still checked against what the tools actually returned; a quote that matches exactly one other fetched page is re-pointed at it and the claimed URL is recorded alongside, because a repair a reader cannot see is a silent rewrite.

Faster

  • Depth now governs the run instead of describing it. It reached the planner as a word in a prompt and nothing else, so "Fast — fewer sources, lowest cost" bought the same eight turns and five pages per task as "Comprehensive". A turn is one model call, and a model call on a hosted router was measured at two to four and a half minutes, so turns are the entire wall-clock of a run. Fast is now 3 turns and 3 pages per task, balanced 5 and 5, comprehensive 8 and 8 — a fast run makes under half the model calls it used to, and the run form states the numbers rather than promising thoroughness.
  • A turn's page reads happen at once. They ran one after another, so a model asking for three pages paid three fetch timeouts end to end. Fetches are capped at ten seconds and now overlap; the executor is also told to request its pages in a single turn, which is the change that removes whole model round-trips rather than shaving seconds off one.

Improved

  • One way to start research. A second start form existed on the older pipeline, was labelled "legacy" in its own banner and was absent from the navigation. It is gone, along with the version vocabulary that ran through the codebase, the API path (/api/v1/v2/runs is now /api/v1/runs) and the interface.
  • Research recorded before runs is still readable, chattable and exportable, listed as Sessions on History and on the project overview rather than by a version number.
  • The import tool is removed. It was a one-shot utility for bringing older research into the current tables, its job is done, and a tool that reads a table nothing else consults is a maintenance cost that misleads. Its outcome table is dropped by a migration.
  • A rework journey covers the report gate end to end, asserting that a rejection authorizes nothing and that the second draft is a new revision rather than an edit of the one that was rejected.

Known

  • Two research pipelines still exist in the backend. The product has one, and research recorded by the earlier one stays readable; consolidating the two is not a patch.
  • Follow-up chat scoped to a single report is available on research recorded as a session and not on a run. Project chat, which cites every approved report in a project, covers both.
  • Cancelling a run still does not interrupt work already in flight; it runs to its next checkpoint, and the tokens spent there are recorded because they were really spent.
  • Claim verification is still not implemented, claim lineage across revisions is still not tracked, and contradiction detection is still source-level and unscored.
  • Citation support is still measured at 0.90 on a single self-judged local-model run, and that measurement predates 2.0.0. It needs re-running before the number is leaned on.
  • Corpus-mode research still has no end-to-end test, because it requires a local embedder and the test environment has none.

v2.0.0 — 2026-08-25

Research becomes a structured record: evidence, claims, sources, conflicts, a human decision, and an artifact anyone can verify offline.

The major number moves because the product's unit of output changed. Before this a run produced a report with citations attached to it. Now the report is a rendering of records that exist in their own right — and those records, not the prose, are what you inspect, review, export, and hand to someone who does not trust this application. The session API, its SSE stream and its bundle format are all still served; nothing that worked against 1.0.2 stops working. See the research record.

Improved

  • A run is a record, not a document. Evidence, sources, claims, claim→evidence links, contradictions, revisions, review decisions, and the approved artifact are first-class rows you can inspect and export.
  • Claims trace to evidence. Every claim resolves to the evidence it was matched against and the source that evidence came from — and a claim that resolved to nothing says so instead of rendering prose that looks supported.
  • Retrieved is not cited, and retrieved is not verified. A source the report never cites keeps no citation number but still appears in Sources. Evidence carries a three-valued provenance state where UNCHECKED means nobody checked, not that it passed.
  • Conflicting sources are a finding, not a footnote — two attributed quotations side by side with the reason they cannot both hold.
  • A run workspace over that record: plan, evidence, claims, sources, conflicts, review, and artifact as views of one run, with live progress that reconnects with Last-Event-ID and replays what it missed rather than restarting.
  • The review screen shows what you are approving before you approve it: claims with and without evidence, cited versus retrieved-only sources, unresolved conflicts, and an unmeasured citation rate reported as unmeasured rather than as 0%.
  • Approval freezes a verifiable artifact. The bundle it produces passes the standalone verifier that ships in this repository — offline, no network, no model, no account.
  • A second human checkpoint before any search spends money. The research plan is reviewed on its own terms: drop a task and it is never researched; reword one and that is what gets searched. Approving a plan never creates an artifact — enforced in the schema, the application, the bundle serialization, and the verifier.
  • Reports are versioned. A rework adds a revision; it never overwrites the one a reviewer already read.
  • Follow-up questions can be scoped to this report, your corpus, the web, or everything, and the answer states which grounding produced it.
  • In-app document preview for PDF, Markdown, text, and HTML from the corpus list. Uploaded HTML renders inside a fully sandboxed frame.
  • History filters by verified-citation rate and by model, so a weak run is findable rather than buried.
  • A one-shot import tool for research recorded by the earlier pipeline, with three verdicts kept separate rather than collapsed into one number: does the imported record say what the original said (fidelity), is the result internally valid (validity), and is every imported fact traceable to something the original recorded (grounding). Removed in 2.0.1 — see below.
  • Server and desktop route parity is now enforced, not intended. Follow-up chat and bundle export previously 404'd on the desktop build; a parity suite now fails the build when a route exists on one host and not the other. Route parity is not feature parity — see Desktop support below for what the desktop build actually runs.
  • Keyless demo mode no longer reaches a real embedding provider. It was billing real API calls on a run the product described as free.
  • Stopping a run now sticks. Cancellation is durable state rather than an advisory event, and every writer that could move a run out of it refuses to. Previously a stopped run could reappear minutes later awaiting your approval, and approving it put a report you had tried to abandon into project memory. Tokens spent between the stop and the pipeline noticing are still recorded, because they were really spent.
  • A run that used scripted models says so. LLM_MODE=fake — which start.sh selects for --fake and silently when no provider key is configured — produced runs recorded as real research: the exported bundle named models nothing had called, at a plausible cost, and the standalone verifier passed it without its demo banner. What actually ran is what gets recorded.

Desktop support in 2.0.0

Research runsThe record (evidence, claims, sources, review, artifact)
Web application (server + worker)Executed by a Celery workerSupported
Desktop application (Tauri + sidecar)Not executableRead and inspect only

The desktop build of 2.0.0 could not execute a run: execute_run acquired a Redis lock, opened the server database engine and checkpointed to Postgres, none of which exist on a host that is SQLite-and-keychain by design. Asked to dispatch a run it answered 501 Not Implemented and created nothing, rather than persisting a run no driver would advance. Verified by running the packaged sidecar, not inferred from the configuration file.

Fixed in 2.0.1: the desktop now drives a run in-process.

Known

  • No production database has been migrated. The tooling is validated against disposable copies — including one restored from real production data, where 11 of 11 sessions migrated with 0 refusals, 0 failures, and 0 fidelity mismatches — but running it on your own data is your decision and your backup.
  • Two states are recorded as unimportable rather than repaired: evidence whose source URL was never recorded, and a plan approval for a run with no plan. Neither occurred in the restored-production run.
  • Some history cannot be recovered at all and is recorded as absent rather than filled in: superseded report drafts, whether a plan was proposed or edited, and whether a run was cancelled. The earlier pipeline overwrote the first two and never recorded the third as a state.
  • Corpus-mode research works but has no end-to-end test, because corpus mode requires a local embedder and the test environment has none.
  • Cancelling a run does not interrupt work already in flight; it runs to its next checkpoint. The decision is durable and authoritative — a cancelled run stays cancelled and cannot reappear awaiting approval — and the tokens spent after the stop are recorded.
  • Claim verification is not implemented: claims are extracted from the report's prose and carry no per-claim judgement. verification_state is UNCHECKED on every claim written.
  • Claim lineage across revisions is not tracked. Nothing observes that a sentence in revision 2 is the assertion from revision 1, and matching by text would manufacture a relationship the system never saw.
  • Project memory does not yet ingest research runs. Fixed in 2.0.1.
  • The run list returns the most recent runs up to a limit and is not paginated.
  • Citation support is still measured at 0.90 on a single self-judged local-model run, and that measurement predates this work. It needs re-running before the number is leaned on.

v1.0.2 — 2026-08-15

Budgets became opt-in, and citation snippets became verifiable.

Improved

  • Every run limit is now opt-in, with 0 meaning unlimited. A hardcoded token ceiling used to kill long runs with no way to raise it.
  • A citation snippet must be text that was actually fetched, so a quote cannot be reconstructed from a model's memory of a page.
  • The evaluation baseline was corrected to stop scoring a competing system against placeholder text.
  • CI waits for the worker to be ready instead of sleeping, and a flaky end-to-end run now fails the gate rather than passing quietly on a retry.

Known

  • Cost caps remain inert on OpenRouter and custom providers, because the pricing catalog cannot price them. Cap spend at the provider.

v1.0.1 — 2026-08-15

Desktop distribution fixes: the bundle actually contained the app.

Improved

  • The desktop bundle ships the engine it needs. The previous build produced a 5 MB app that passed CI, uploaded cleanly, and died on first launch.
  • Release assets are checksummed under the names they are actually served with, so verification succeeds instead of silently checking nothing.

Known

  • Builds are unsigned. macOS and Windows both warn on first launch; the download page explains the unblock steps before you download rather than leaving the OS to explain after.

v1.0.0 — 2026-08-14

First release: the pipeline, the human gate, and verifiable exports.

Improved

  • Planner, executor, critic, and synthesizer running as a graph, with a durable human approval checkpoint before anything is finalised.
  • Every citation resolves to a source and a verbatim snippet; one that cannot be verified renders a warning chip instead of rendering clean.
  • Markdown, PDF, and hash-verifiable bundle exports, with a standalone offline verifier.
  • Self-hosting with Docker, bring-your-own-key, and local models through Ollama.

Known

  • Project memory is Postgres-only, so the desktop build has no cross-report memory.

Versioning

Semantic-ish: the patch number moves for fixes, the minor for features, and the major moves for a break in a documented contract — the API, the SSE event shapes, or the bundle format, which carries its own bundle_version besides.

v2.0.0 is the one deliberate exception, and it is worth stating rather than glossing. It breaks no documented contract: the session API, its SSE stream and its bundle format are all still served, and a client written against 1.0.2 keeps working. The major moved because the product's unit of output changed — from a report with citations attached to a structured record that a report is rendered from — and calling that a minor release would have understated it to exactly the people who read a changelog to decide whether to trust the thing.