From 63cf896304dc72963a1dd4fd759235a984df68f9 Mon Sep 17 00:00:00 2001 From: sail3r <82087684+sail3r@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:41:44 +1300 Subject: [PATCH 1/4] Web Search: the pane shows free-text dials; bump mcp-cpp mcp-cpp (feat/search-dials, 791957d, 5 commits on edc3186): - adds dials to most services and lists every documented value on the existing ones; a service may now declare up to seven dials; - adds free-text dials: Brave API's Goggle (a hosted Goggle URL) and Firecrawl's Location; - reads Firecrawl's data.research. From 2026-11-16 the Research category answers with paper records there, and without this a research search would be reported unreadable. Each paper gets a link to its own page (arXiv, PubMed, PubMed Central, doi.org), or to Firecrawl's paper page for any other kind of id; - sends Never return sites to Perplexity as its domain filter (search_domain_filter, "-" in front of each host, at most 20) instead of -site: words in the query. Perplexity documents the deny form for its Search API; -site: in the query was never documented there. The pane built every dial as a dropdown, and apply_form read only dropdown rows. A free-text dial (WebSearchDial::text set) is now a Text row. Its value is trimmed and checked with mcp-cpp's own web_search_dial_value_ok before it is stored, so the pane never saves a value a search would drop: a value that fails is not stored and the rebuild shows what was on record. An emptied row erases the entry, as a dropdown set back to its default does. Text rows commit on leaving the field, as the registry's text rows already do. Stored dial values were capped at 64 bytes on load and save (dial_entry_ok). A Goggle URL can be longer, so the value cap is now kMaxDialValueBytes (2048), the longest text dial mcp-cpp declares; the key cap and the 64-entry cap are unchanged. All services together have 45 dials, under the 64-entry cap. github is no longer a Firecrawl category, so the plan test that used it uses developer. web_search_config.hpp's note on -site: support lists Perplexity among the services with a native domain filter. --- include/agentty/domain/web_search_config.hpp | 17 ++++--- .../agentty/runtime/panel/web_search_form.hpp | 4 +- src/domain/web_search_config.cpp | 3 +- src/runtime/panel/web_search_form.cpp | 48 +++++++++++++++---- tests/web_search_services_test.cpp | 45 ++++++++++++++++- third_party/mcp-cpp | 2 +- 6 files changed, 100 insertions(+), 19 deletions(-) diff --git a/include/agentty/domain/web_search_config.hpp b/include/agentty/domain/web_search_config.hpp index fdc6b19cf..425cabe9e 100644 --- a/include/agentty/domain/web_search_config.hpp +++ b/include/agentty/domain/web_search_config.hpp @@ -30,9 +30,10 @@ // What is and is not known about `-site:` support: DuckDuckGo documents it; // Brave honours it (checked live: a query that returns ten pinterest.com hits // returns none with `-site:pinterest.com`; Brave's own docs spell the operator -// `NOT site:`). Services with a native domain filter (Tavily, Firecrawl, Exa) -// receive the list as a request field instead, which mcp-cpp does by lifting -// the `-site:` tokens back out of the query. +// `NOT site:`). Services with a native domain filter (Tavily, Firecrawl, Exa, +// Perplexity) receive the list as a request field instead, which mcp-cpp does +// by lifting the `-site:` tokens back out of the query (at most 20; any more +// stay in the query). // // Pure: no IO, no globals. The tool layer installs a value of this type // (tool/web_search_policy.hpp); the runtime persists it through the settings @@ -120,20 +121,24 @@ struct Config { std::string language = "auto"; // Per-SERVICE extra settings (freshness, region, ...), keyed - // "." -> value id. Per service rather than per slot + // "." -> value id, or the typed text for a free-text + // dial (a Brave Goggle URL). Per service rather than per slot // so choosing Exa for Fallback after trying it as Primary keeps what was // set. Only non-default values are stored. Values are not checked here: - // mcp-cpp sends a value only if the service lists it. + // mcp-cpp sends a value only if it passes web_search_dial_value_ok. std::map dials; [[nodiscard]] bool operator==(const Config&) const = default; }; // What a stored dial entry must look like: key "." and a value, -// both short and printable. Applied on load AND on save, so a hand-edited +// both printable, the key short and the value within kMaxDialValueBytes. +// Applied on load AND on save, so a hand-edited // settings.json cannot hold what the pane could not produce, and junk keys // cannot crowd real ones out of the cap. inline constexpr std::size_t kMaxDials = 64; +// Room for the longest free-text dial mcp-cpp declares: a 2048-byte Goggle URL. +inline constexpr std::size_t kMaxDialValueBytes = 2048; [[nodiscard]] bool dial_entry_ok(std::string_view key, std::string_view value) noexcept; // The typed view of Config::mode_text. Anything unrecognised reads as Auto — diff --git a/include/agentty/runtime/panel/web_search_form.hpp b/include/agentty/runtime/panel/web_search_form.hpp index d91ece2ee..b5a84a721 100644 --- a/include/agentty/runtime/panel/web_search_form.hpp +++ b/include/agentty/runtime/panel/web_search_form.hpp @@ -27,7 +27,9 @@ using KeyOrigin = std::function; // Row ids beyond the registry's. A service's dials and key hang under the // slot that names it (the first slot only, when two name the same service). -// web_search.dial.. a Choice; value id "" = the service default +// web_search.dial.. a Choice (value id "" = the service +// default), or Text for a free-text dial +// (empty = the default) // web_search.key. a Secret; always empty, see the .cpp inline constexpr std::string_view kDialPrefix = "web_search.dial."; inline constexpr std::string_view kKeyPrefix = "web_search.key."; diff --git a/src/domain/web_search_config.cpp b/src/domain/web_search_config.cpp index 532a233c8..1be92a6a9 100644 --- a/src/domain/web_search_config.cpp +++ b/src/domain/web_search_config.cpp @@ -173,7 +173,8 @@ nlohmann::json rewrite_args(const Config& c, nlohmann::json args) { } bool dial_entry_ok(std::string_view key, std::string_view value) noexcept { - if (key.empty() || key.size() > 64 || value.empty() || value.size() > 64) return false; + if (key.empty() || key.size() > 64 || value.empty() || value.size() > kMaxDialValueBytes) + return false; const auto dot = key.find('.'); if (dot == std::string_view::npos || dot == 0 || dot + 1 == key.size()) return false; auto printable = [](std::string_view s) { diff --git a/src/runtime/panel/web_search_form.cpp b/src/runtime/panel/web_search_form.cpp index 1aba06d8a..332367830 100644 --- a/src/runtime/panel/web_search_form.cpp +++ b/src/runtime/panel/web_search_form.cpp @@ -38,6 +38,25 @@ std::string dial_key(std::string_view service, std::string_view dial) { return k; } +std::string trim(std::string_view s) { + constexpr std::string_view kSpace = " \t"; + const auto b = s.find_first_not_of(kSpace); + if (b == std::string_view::npos) return {}; + return std::string{s.substr(b, s.find_last_not_of(kSpace) - b + 1)}; +} + +// Whether `value` may be stored for the text dial `key` ("."): +// mcp-cpp's own rule, so the pane never saves what a search would drop. +bool text_dial_ok(std::string_view key, std::string_view value) { + const auto dot = key.find('.'); + if (dot == std::string_view::npos) return false; + const auto* svc = ::mcp::tools::find_web_search_service(key.substr(0, dot)); + if (!svc) return false; + for (const auto& d : svc->dials) + if (d.id == key.substr(dot + 1)) return ::mcp::tools::web_search_dial_value_ok(d, value); + return false; +} + // "Brave Free \xe2\x86\x92 DuckDuckGo Free \xe2\x86\x92 Tavily Free" for the subtitle. std::string chain_text(const web_search_cfg::Config& c) { std::string out; @@ -72,14 +91,19 @@ void add_service_rows(form::Builder& b, const web_search_cfg::Config& c, const ::mcp::tools::WebSearchService& svc, const KeyOrigin& key_origin) { for (const auto& d : svc.dials) { - std::vector labels, ids; - for (const auto& v : d.values) { labels.push_back(v.label); ids.push_back(v.id); } std::string cur; if (auto it = c.dials.find(dial_key(svc.id, d.id)); it != c.dials.end()) cur = it->second; std::string help = svc.label + (d.help.empty() ? "" : " \xc2\xb7 " + d.help); - b.choice(std::string{kDialPrefix} + dial_key(svc.id, d.id), - " " + d.label, std::move(labels), std::move(ids), cur, std::move(help)); + std::string row_id = std::string{kDialPrefix} + dial_key(svc.id, d.id); + if (d.text != ::mcp::tools::WebSearchText::None) { + b.text(std::move(row_id), " " + d.label, cur, std::move(help)); + } else { + std::vector labels, ids; + for (const auto& v : d.values) { labels.push_back(v.label); ids.push_back(v.id); } + b.choice(std::move(row_id), " " + d.label, std::move(labels), std::move(ids), + cur, std::move(help)); + } b.origin(cur.empty() ? "default" : "settings.json"); } if (!svc.needs_key) return; @@ -214,13 +238,21 @@ void apply_form(const form::Form& f, web_search_cfg::Config& c) { reg::read_rows(c, f, reg::Owner::WebSearch); // Dial rows. They are not registry rows (which dials exist depends on the // services chosen), so they are found by their id prefix. A dial set back - // to the service's own default is erased, not stored as "". + // to the service's own default (or a text row emptied) is erased, not + // stored as "". Typed text mcp-cpp would not send is not stored either: + // the rebuild then shows the row with what was on record. for (const auto& row : f.fields) { if (row.locked || row.id.rfind(kDialPrefix, 0) != 0) continue; - const auto* ch = std::get_if(&row.value); - if (!ch) continue; const std::string key = row.id.substr(kDialPrefix.size()); - const std::string val{ch->id()}; + std::string val; + if (const auto* ch = std::get_if(&row.value)) { + val = std::string{ch->id()}; + } else if (const auto* tx = std::get_if(&row.value)) { + val = trim(tx->value); + if (!val.empty() && !text_dial_ok(key, val)) continue; + } else { + continue; + } if (val.empty()) c.dials.erase(key); else c.dials[key] = val; } diff --git a/tests/web_search_services_test.cpp b/tests/web_search_services_test.cpp index 4a87afc0d..658762e29 100644 --- a/tests/web_search_services_test.cpp +++ b/tests/web_search_services_test.cpp @@ -217,14 +217,14 @@ TEST_CASE("web search plan: keys go to keyed services only, and a missing key st TEST_CASE("web search plan: dials follow their service, not their slot") { sc::Config c; c.primary = "firecrawl-free"; c.secondary = "tavily-free"; c.fallback = "none"; - c.dials["firecrawl-free.category"] = "github"; + c.dials["firecrawl-free.category"] = "developer"; c.dials["tavily-free.topic"] = "news"; c.dials["exa-api.type"] = "fast"; // not in the plan auto p = sp::build_plan(c, no_keys); REQUIRE(p.slots.size() == 2); REQUIRE(p.slots[0].dials.size() == 1); CHECK(p.slots[0].dials[0].first == "category"); - CHECK(p.slots[0].dials[0].second == "github"); + CHECK(p.slots[0].dials[0].second == "developer"); REQUIRE(p.slots[1].dials.size() == 1); CHECK(p.slots[1].dials[0].first == "topic"); @@ -257,6 +257,10 @@ TEST_CASE("web search settings: a dial entry must look like .") { CHECK(!sc::dial_entry_ok("brave-free.freshness", "")); CHECK(!sc::dial_entry_ok("brave-free.freshness", "p\nw")); CHECK(!sc::dial_entry_ok(std::string(65, 'a') + ".x", "pw")); + // A value has room for the longest free-text dial (a Goggle URL), no more. + const std::string url = "https://" + std::string(sc::kMaxDialValueBytes - 8, 'g'); + CHECK(sc::dial_entry_ok("brave-api.goggles", url)); + CHECK(!sc::dial_entry_ok("brave-api.goggles", url + "g")); } // ── The key store ──────────────────────────────────────────────────────── @@ -480,6 +484,40 @@ TEST_CASE("web search form: a dial round-trips through apply_form, and the defau CHECK(c.dials.find("brave-free.freshness") == c.dials.end()); } +TEST_CASE("web search form: a text dial is a Text row, checked before it is stored") { + const std::string goggle = + "https://raw.githubusercontent.com/brave/goggles-quickstart/main/goggles/tech_blogs.goggle"; + sc::Config c; + c.primary = "brave-api"; + auto f = wtf::build_form(c, kNoEnv); + auto* row = f.find("web_search.dial.brave-api.goggles"); + REQUIRE(row != nullptr); + auto* tx = std::get_if(&row->value); + REQUIRE(tx != nullptr); + CHECK(tx->value.empty()); + + // Typed with stray spaces: stored trimmed, and shown again on rebuild. + tx->value = " " + goggle + " "; + wtf::apply_form(f, c); + CHECK(c.dials.at("brave-api.goggles") == goggle); + auto g = wtf::build_form(c, kNoEnv); + CHECK(std::get(g.find("web_search.dial.brave-api.goggles")->value).value + == goggle); + + // Something a search would not send is not stored; the old value stays. + for (const char* bad : {"http://example.com/a.goggle", "$boost=3,site=dev.to", + "https://a.com/x y"}) { + std::get(g.find("web_search.dial.brave-api.goggles")->value).value = bad; + wtf::apply_form(g, c); + CHECK(c.dials.at("brave-api.goggles") == goggle, bad); + } + + // Emptied: the entry is removed, as a list dial set back to its default is. + std::get(g.find("web_search.dial.brave-api.goggles")->value).value = " "; + wtf::apply_form(g, c); + CHECK(c.dials.find("brave-api.goggles") == c.dials.end()); +} + TEST_CASE("web search form: choosing a slot's service changes the rows under it") { sc::Config c; c.primary = "exa-api"; @@ -513,6 +551,8 @@ TEST_CASE("web search settings: dials and slots persist, and a key never does") s.web_search.secondary = "none"; s.web_search.language = "de"; s.web_search.dials["exa-api.type"] = "fast"; + s.web_search.dials["brave-api.goggles"] = + "https://raw.githubusercontent.com/brave/goggles-quickstart/main/goggles/tech_blogs.goggle"; REQUIRE(ss::store("exa-api", "exa-LEAKCHECK-0123456789")); persistence::save_settings(::agentty::IoAccess::grant(), agtest::cap(), s); @@ -527,6 +567,7 @@ TEST_CASE("web search settings: dials and slots persist, and a key never does") CHECK(back.web_search.fallback == "tavily-free"); // untouched default CHECK(back.web_search.language == "de"); CHECK(back.web_search.dials.at("exa-api.type") == "fast"); + CHECK(back.web_search.dials.at("brave-api.goggles").size() > 64); // over the old cap } TEST_CASE("web search settings: a hand-edited bad value cannot reach a slot") { diff --git a/third_party/mcp-cpp b/third_party/mcp-cpp index edc318660..791957dd0 160000 --- a/third_party/mcp-cpp +++ b/third_party/mcp-cpp @@ -1 +1 @@ -Subproject commit edc318660bec8578875d73d4f481c3fe45dd11d3 +Subproject commit 791957dd0bcf41f7bc62d3f88b532fb866d1612f From e0952145422b6f6c4ae92cfde10141a035ce9992 Mon Sep 17 00:00:00 2001 From: sail3r <82087684+sail3r@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:41:50 +1300 Subject: [PATCH 2/4] docs: Web Search's new per-service settings and typed rows --- CHANGELOG.md | 20 ++++++++++++++++++++ docs/website/configuration.md | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a249dd30f..e52e7c49c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to agentty. Versions follow [SemVer](https://semver.org/). ## [Unreleased] +### Added +- **Web Search: more settings per service.** Every new setting changes + which results come back, not what a search costs. Tavily (both tiers) + gains *Exact phrases*, *Language match*, *Country* and *Safe search*, + and Tavily Free gains *Depth*. Brave API gains *Spell correction*. + Firecrawl gains *Safe search*, *Sort* (newest first) and *Location*. + Exa gains *Safe search* and *Country*, and Perplexity gains *Mode*. + Brave API also takes a *Goggle*: the URL of a hosted rules file that + re-ranks its results. *Goggle* and *Location* are typed rows, saved only + when the service would accept them. + +### Changed +- **Web Search: dials list every documented value.** Past hour on Firecrawl, + Serper and Exa. All six Exa modes, including deep-lite and deep-reasoning + (deep-reasoning can take 12-40 seconds, longer than the 30-second request + limit). Brave's *All regions* country. All 68 DuckDuckGo regions. + Firecrawl's categories are now Developer, Research, PDF and Government; + GitHub is gone from Firecrawl's API, so a saved GitHub choice is no longer + sent. + ## [0.9.25] - 2026-10-16 ### Changed diff --git a/docs/website/configuration.md b/docs/website/configuration.md index 6bdcda4b5..85bc8d36f 100644 --- a/docs/website/configuration.md +++ b/docs/website/configuration.md @@ -98,7 +98,7 @@ The **Free** services need no account, card or key, but they are best effort: Br The **API key** services are the vendors' documented interfaces and do not depend on a page layout. Paste a key into the row under the slot that uses it and press Enter. It is stored encrypted (the OS keystore when `AGENTTY_USE_KEYSTORE` is set, otherwise a machine-bound sealed file) and is never written to `settings.json`; the row then reads `✓ key saved` and stays locked until *Remove key* forgets it. A key from `AGENTTY_WEB_SEARCH_KEY_` reads `● key from` the variable and is locked for as long as it is set. A keyed service chosen with no key is reported as such in the search result and the next slot is tried. -Each service offers up to three extra settings (freshness, region, category, ...) which appear under its slot, and none for a service that has none. They are saved per service in `settings.json` under `web_search.dials`, so a choice survives moving the service between slots. +Each service offers up to seven extra settings which appear under its slot, and none for a service that has none. Most are a list to pick from: freshness, region, safe search, spell correction, exact phrases, sort order, and so on. Two are typed: Brave API's *Goggle* takes the `https://` URL of a hosted Goggle (a rules file on GitHub, GitLab or a Gist, registered at [search.brave.com/goggles/create](https://search.brave.com/goggles/create)) that re-ranks Brave's results, and Firecrawl's *Location* takes a place such as `San Francisco,California,United States`. A typed value is saved when you leave the row, and only if it is one the service accepts; otherwise the row goes back to what was saved. Empty the row to clear it. Settings are saved per service in `settings.json` under `web_search.dials`, so a choice survives moving the service between slots. Some only take effect together with another: Tavily's *Country* applies with the General topic, its *Safe search* not with the fast depths, and *Language match* only when a language is set. Kagi, Mojeek and Linkup are not offered yet: their request and response formats could not be verified against documentation or a real reply. From 8dec03d29003de81b8269de605e79d27788b35d6 Mon Sep 17 00:00:00 2001 From: sail3r <82087684+sail3r@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:03:58 +1300 Subject: [PATCH 3/4] docs: changelog for Firecrawl research results --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52e7c49c..1b087355d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ All notable changes to agentty. Versions follow [SemVer](https://semver.org/). Firecrawl's categories are now Developer, Research, PDF and Government; GitHub is gone from Firecrawl's API, so a saved GitHub choice is no longer sent. +- **Web Search: Firecrawl's Research category keeps working after + 2026-11-16.** From that date Firecrawl answers it with research papers + instead of web pages. Each paper is shown with a link to its page on + arXiv, PubMed, PubMed Central or doi.org, or to Firecrawl's own page for + the paper when it has none of those, and its abstract as the snippet. ## [0.9.25] - 2026-10-16 From 7b42466848576b6767b229214afa3fe1d9c6e646 Mon Sep 17 00:00:00 2001 From: sail3r <82087684+sail3r@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:44:10 +1300 Subject: [PATCH 4/4] docs: Never return reaches Perplexity's own domain filter --- CHANGELOG.md | 5 +++++ docs/website/configuration.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b087355d..53465ec40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ All notable changes to agentty. Versions follow [SemVer](https://semver.org/). instead of web pages. Each paper is shown with a link to its page on arXiv, PubMed, PubMed Central or doi.org, or to Firecrawl's own page for the paper when it has none of those, and its abstract as the snippet. +- **Web Search: Never return reaches Perplexity's own domain filter.** + Excluded sites used to be added to Perplexity searches as `-site:` + words, which Perplexity does not document. They are now sent through its + documented exclude filter, as for Tavily, Firecrawl and Exa. Perplexity + takes up to 20; any more are still added as `-site:`. ## [0.9.25] - 2026-10-16 diff --git a/docs/website/configuration.md b/docs/website/configuration.md index 85bc8d36f..a69b71879 100644 --- a/docs/website/configuration.md +++ b/docs/website/configuration.md @@ -87,7 +87,7 @@ The Smart Mode *feature* toggles (which layers run) live in the `Ctrl+S` overlay | `AGENTTY_WEB_SEARCH` | `auto` (default), `on`, or `off`. `auto` advertises `web_search` and leaves the result count to the model, as agentty did before the Web Search pane. `on` applies your `AGENTTY_WEB_SEARCH_COUNT` and `_MAX`. `off` removes `web_search` from every request, from the tool list `agentty mcp-serve` offers to other MCP clients, and refuses any call to it (a replayed thread, a call injected by an ACP client). `0`/`false`/`no` mean `off`; `1`/`true`/`yes` mean `auto` (search available, the model picks the count). | | `AGENTTY_WEB_SEARCH_COUNT` | Results when the model does not say how many. Applies only in `on`. Default `10`; range 1–20. | | `AGENTTY_WEB_SEARCH_MAX` | Most results the model may ask for in one search; a larger request is clamped, and `0` or a negative count becomes `1`. Applies only in `on`. Default `20`; range 1–50. A search returns at most one result page from the service that answers, and each service caps it: 20 on Brave, Tavily, Serper and Perplexity, 30 on DuckDuckGo, 100 on Firecrawl and Exa. A larger request is cut to that cap. | -| `AGENTTY_WEB_SEARCH_EXCLUDE` | Domains never to return, space- or comma-separated (`pinterest.com w3schools.com`). Pasted URLs and `www.` are reduced to the host. Sent as `-site:` operators to Brave, DuckDuckGo, Serper and Perplexity, and as the service's own domain filter to Tavily, Firecrawl and Exa, so an excluded site never comes back and its slot goes to another result. | +| `AGENTTY_WEB_SEARCH_EXCLUDE` | Domains never to return, space- or comma-separated (`pinterest.com w3schools.com`). Pasted URLs and `www.` are reduced to the host. Sent as `-site:` operators to Brave, DuckDuckGo and Serper, and as the service's own domain filter to Tavily, Firecrawl, Exa and Perplexity, so an excluded site never comes back and its slot goes to another result. Up to 20 sites go in the filter; any beyond that are added to the query as `-site:`. | | `AGENTTY_WEB_SEARCH_PRIMARY`, `_SECONDARY`, `_FALLBACK` | Which service answers first, second and last; the first to succeed wins. A service id: `brave-free`, `ddg-free`, `tavily-free`, `firecrawl-free` (no account, card or key) or `brave-api`, `tavily-api`, `firecrawl-api`, `exa-api`, `serper-api`, `perplexity-api` (need an API key). `_SECONDARY` and `_FALLBACK` also take `none`. Defaults: `brave-free`, `ddg-free`, `tavily-free`. | | `AGENTTY_WEB_SEARCH_LANGUAGE` | `auto` (default) or a two-letter code (`en`, `de`, `ja`, ...) sent to the services that take one: Tavily, Brave API, Serper, Perplexity. The others ignore it. | | `AGENTTY_WEB_SEARCH_KEY_` | An API key, taken from the environment instead of the key store: `AGENTTY_WEB_SEARCH_KEY_EXA_API`, `_BRAVE_API`, `_TAVILY_API`, `_FIRECRAWL_API`, `_SERPER_API`, `_PERPLEXITY_API`. Wins over a stored key and locks that row in the pane. Never written to disk. |