Found by re-framing the reproduction: instead of scripting against a provider, configure our own containers the way an external plane configures theirs and exercise the real MCP tools. The change turned out to be impossible to express through the canonical profile — which is the finding.
The Problem
ai/configBase.mjs declares six ollama env leaves. ai/deploy/docker-compose.yml passes one.
declared leaf
reaches the containers?
NEO_OLLAMA_KEEP_ALIVE
✅
NEO_OLLAMA_HOST
❌
NEO_OLLAMA_MODEL
❌
NEO_OLLAMA_EMBEDDING_MODEL
❌
NEO_OLLAMA_EMBEDDING_TIMEOUT_MS
❌
NEO_OLLAMA_REQUIRE_PARALLEL_MODELS
❌
NEO_OLLAMA_HOST appears zero times across every compose variant in ai/deploy/ (docker-compose.yml, .dev, .local-agent-os, .parity-capture, .parity-ci, .test).
So a deployment that selects NEO_EMBEDDING_PROVIDER=ollama through the canonical profile gets ollama.host resolved to its leaf default, http://127.0.0.1:11434. Inside the kb-server container that address is the kb-server itself. Nothing listens on it. Every native ollama embed fails with connection-refused, permanently, on any plane configured through this profile. The model service is reachable at http://local-model:11434 on the compose network.
Bounded correctly, because the first wording overclaimed: a hand-written or out-of-band Compose could always supply the variable, and a deployment profile we maintain does exactly that.
Attribution corrected 2026-08-10 11:1xZ. An earlier wording said an external plane's own Compose hand-writes the variable, and used that as field evidence of cost borne downstream. Git blame says otherwise: @tobiu wrote those lines himself, on 2026-05-26, in a Compose file our side maintains, in a commit titled "feat(compose): R1 MCP stack mirroring neo canonical ai/deploy" — 41 of that file's commits are his. So the re-derivation cost was ours, not an external operator's, and presenting it as the latter inflated the defect's reach.
It remains evidence, and arguably better evidence: the person mirroring the canonical profile could not express the composition through it either, and silently hand-wired around the gap. A profile that its own maintainers must work around is incomplete on the clearest possible witness. But it is a statement about us, and it was attributed to someone else. The defect is not that the composition is impossible — it is that the canonical profile cannot express it, so everyone who wants it must independently discover a missing variable and inject it by a route no committed artifact records.
The other four missing leaves compound it: the operator's chosen chat model, embedding model and timeout are all silently ignored in favour of leaf defaults.
The contrast is what makes this an omission rather than a policy. The sibling provider is fully wired — NEO_OPENAI_COMPATIBLE_HOST, _MODEL, _EMBEDDING_MODEL, _API_KEY, _KEEP_ALIVE all pass through. And NEO_OLLAMA_KEEP_ALIVEis passed: nobody wires a keep-alive for a provider they intend to exclude. The intent to support ollama is in the file; the host never followed.
Why it is worse than a missing variable
embeddingProvider: leaf('openAiCompatible', 'NEO_EMBEDDING_PROVIDER', 'string') presents ollama as a first-class choice, and the code backs that up completely: ai/provider/Ollama.mjs is a full provider, TextEmbeddingService has a native embed path, providerReadinessHelper carries a whole ollama branch keyed on role === 'embedding' ? '/api/embed' : '/api/chat'.
Everything an operator can read says ollama is supported. The canonical deployment layer cannot deliver it. Someone reading our config and setting the obvious value gets a plane whose corpus never fills, with no error naming the cause — the failure is a connection refused to localhost, which reads as "the provider is down" rather than "the host was never configured".
The Architectural Reality
ai/deploy/docker-compose.yml — the three service env blocks (kb-server ~`:88, mc-server ~:210, orchestrator ~:308). Each passes NEO_EMBEDDING_PROVIDERand the fullNEO_OPENAI_COMPATIBLE_*set, plusNEO_OLLAMA_KEEP_ALIVE` alone.
Same file, local-model service — ollama/ollama:latest, OLLAMA_HOST=0.0.0.0:11434, reachable on the network as local-model:11434, gated behind --profile local-model. The target exists; nothing can be pointed at it.
Corrected after @neo-gpt's review on PR #16851, which falsified the original ledger: it specified a file-wide declared-leaf subset while the production claim is per consuming service, and it treated all six leaves as symmetric. The unit of the contract is a (service, leaf)coordinate.
TextEmbeddingService (#getOllamaProvider, #getOllamaEmbeddingTimeoutMs) + the dispatch seam, reachable from the kb entrypoint
mc-server × the same five
docker-compose.yml
passed through as ${VAR:-}
unset ⇒ leaf default
memory embedding + graph extractors run in-process
orchestrator × the same five plus REQUIRE_PARALLEL_MODELS
docker-compose.yml
passed through as ${VAR:-}
unset ⇒ leaf default
buildOllamaReadinessConfig, the configured serve task, DreamService, the residency repair
kb-server / mc-server × REQUIRE_PARALLEL_MODELS
—
deliberately NOT passed
n/a
sole read is buildOllamaReadinessConfig; all three callers are under ai/daemons/orchestrator/. The module is in both import graphs, but only because TextEmbeddingService dynamic-imports fetchLmsLoadedModels from it — module reachability is not consumption
NEO_OLLAMA_HOST on a --profile local-model plane
operator .env
can be set to http://local-model:11434 and take effect
plane check: native embed reaches the model service
Decision Record impact:aligned-with ADR 0019 — the leaves already own env-override-with-default; this is the deployment layer failing to deliver the env, not a config-resolution change. No leaf definitions are touched.
Acceptance Criteria
Every declared NEO_OLLAMA_* leaf reaches each service that consumes it, and no service carries one it does not. Receipt:kb-server and mc-server carry 5; orchestrator carries 6. The original wording of this AC ("all six … to every service") was wrong, and the review that caught the guard caught the ledger with it: shipping REQUIRE_PARALLEL_MODELS to kb/mc advertises an orchestrator knob as theirs.
A mechanical check asserts declared-NEO_OLLAMA_*-leaves ⊆ compose-passed-vars … the same check applied to NEO_OPENAI_COMPATIBLE_* must pass on the current tree.This AC was WRONG and implementing it is what proved so. The invariant is false by design: 8 of 14 declared NEO_OPENAI_COMPATIBLE_* leaves legitimately never reach Compose (BATCH_EMBEDDING_*, CONTENTION_*, UNLOAD_RETRY_*, REQUIRE_PARALLEL_MODELS), because most leaves are tunables whose defaults are authoritative. The required positive control would have failed.
Replaced by the invariant that is true and that catches this defect: the contract is a set of (service, leaf) coordinates.Receipt:test/playwright/unit/ai/deploy/OllamaProviderEnvCoordinates.spec.mjs asserts each required pair independently, requires each value to be a ${NAME:-} interpolation of its own variable, and treats a NEO_OLLAMA_* entry with no consumer behind it as a violation rather than a harmless extra.
Mutation-convicted, and the mutations are permanent tests rather than a one-off claim — coordinateViolations() is a pure function of a compose document, so the suite runs it against deliberately broken clones. All four go red: dropping a required leaf from one service; hardcoding the value to a wrong target; interpolating a different variable; adding a coordinate by symmetry. The first two are @neo-gpt's exact-head falsifiers against the predecessor guard, which passed both.
Every required coordinate carries a receipt — the consuming module plus the literal source lines that perform the read, matched with whitespace normalised and checked against that service's import graph — so a coordinate cannot be added by assertion alone. Every deliberate absence carries a written reason. A coverage test requires both tables to span the full (service × declared-leaf) product, so a newly declared leaf reddens the suite until its disposition is decided per service.
The weaker family-agnostic floor (a loopback-default leaf must be overridable somewhere in the profile) remains in LoopbackDefaultReachability.spec.mjs, now stating in its own header that it cannot see service scope and must not be read as "wired correctly".
Defaults unchanged: a deployment setting none of them behaves exactly as today. Receipt: every entry is ${VAR:-}, so an unset var yields an empty value and the leaf default stands; 417 passed across ai/deploy/ and ai/scripts/lint/.
On a --profile local-model plane with NEO_EMBEDDING_PROVIDER=ollama and NEO_OLLAMA_HOST=http://local-model:11434, a native embed reaches the model service instead of container-localhost. [L3-deferred — operator handoff needed] — a running compose plane is not reachable from unit CI, so this is carried as Post-Merge Validation on the PR rather than gating merge.
Partial receipt already in hand, stated as partial: a CPU-only ollama/ollama:0.23.1 container mounted on the canonical local-model-data volume served POST /api/embed for qwen3-embedding — http=200, 4096 dims, runner CPU returning to 0.00 % after the call. That proves the model and the wire work in a container. It does not prove the env reaches kb-server, which is the actual claim here and needs the plane.
Out of Scope
Changing the default provider. openAiCompatible stays the default.
Whether ollama-native or openAiCompatible-against-ollama is the recommended path — this ticket makes the declared choice work; it does not re-open which choice to prefer.
#16849 (Ollama.stream() timeout), #16846 (embedding batch env overrides) — same neighbourhood, separate defects.
Avoided Traps
Reading the omission as policy. The wired NEO_OLLAMA_KEEP_ALIVE and the fully-implemented provider, readiness-probe and embed paths all say ollama is meant to work. One passed variable out of six is an oversight with a long blast radius, not a decision.
A one-sided guard. An assertion that only checks the ollama arm would pass trivially once this ships and never protect the sibling.
A guard that restates the fix instead of testing it. The first version checked a union of env names across services and stayed green under both of the mutations it existed to catch. A name appearing somewhere in a file is not a deployment contract.
Deriving the consumer set from an import graph. The convenient instrument says all six leaves have readers in all three graphs, which would have restored the symmetry copy with machinery behind it. Module presence is the coarse signal; the call path is the finer one and it was right there.
Claiming this explains a specific plane's empty corpus. It explains any plane configured through this profile with NEO_EMBEDDING_PROVIDER=ollama. Whether a given deployment is in that state is a one-command check (docker exec <kb-container> printenv NEO_EMBEDDING_PROVIDER NEO_OLLAMA_HOST), not something this ticket may assume.
Related
#16706 — deployment-stability epic; the operator runbook gains this as a discriminator.
#16830 — stuck-runner detection on containerized planes, which probes the same native surface.
#16846, #16849 — adjacent provider/embedding defects found in the same sweep.
Live latest-open sweep: latest 12 open issues plus a scoped title sweep for ollama/compose/env duplicates at 2026-08-10T00:5xZ; no equivalent found.
Structure map: N/A — modifies ai/deploy/docker-compose.yml in place.
Context
Found by re-framing the reproduction: instead of scripting against a provider, configure our own containers the way an external plane configures theirs and exercise the real MCP tools. The change turned out to be impossible to express through the canonical profile — which is the finding.
The Problem
ai/configBase.mjsdeclares six ollama env leaves.ai/deploy/docker-compose.ymlpasses one.NEO_OLLAMA_KEEP_ALIVENEO_OLLAMA_HOSTNEO_OLLAMA_MODELNEO_OLLAMA_EMBEDDING_MODELNEO_OLLAMA_EMBEDDING_TIMEOUT_MSNEO_OLLAMA_REQUIRE_PARALLEL_MODELSNEO_OLLAMA_HOSTappears zero times across every compose variant inai/deploy/(docker-compose.yml,.dev,.local-agent-os,.parity-capture,.parity-ci,.test).So a deployment that selects
NEO_EMBEDDING_PROVIDER=ollamathrough the canonical profile getsollama.hostresolved to its leaf default,http://127.0.0.1:11434. Inside thekb-servercontainer that address is the kb-server itself. Nothing listens on it. Every native ollama embed fails with connection-refused, permanently, on any plane configured through this profile. The model service is reachable athttp://local-model:11434on the compose network.Bounded correctly, because the first wording overclaimed: a hand-written or out-of-band Compose could always supply the variable, and a deployment profile we maintain does exactly that.
The other four missing leaves compound it: the operator's chosen chat model, embedding model and timeout are all silently ignored in favour of leaf defaults.
The contrast is what makes this an omission rather than a policy. The sibling provider is fully wired —
NEO_OPENAI_COMPATIBLE_HOST,_MODEL,_EMBEDDING_MODEL,_API_KEY,_KEEP_ALIVEall pass through. AndNEO_OLLAMA_KEEP_ALIVEis passed: nobody wires a keep-alive for a provider they intend to exclude. The intent to support ollama is in the file; the host never followed.Why it is worse than a missing variable
embeddingProvider: leaf('openAiCompatible', 'NEO_EMBEDDING_PROVIDER', 'string')presentsollamaas a first-class choice, and the code backs that up completely:ai/provider/Ollama.mjsis a full provider,TextEmbeddingServicehas a native embed path,providerReadinessHelpercarries a whole ollama branch keyed onrole === 'embedding' ? '/api/embed' : '/api/chat'.Everything an operator can read says ollama is supported. The canonical deployment layer cannot deliver it. Someone reading our config and setting the obvious value gets a plane whose corpus never fills, with no error naming the cause — the failure is a connection refused to localhost, which reads as "the provider is down" rather than "the host was never configured".
The Architectural Reality
ai/deploy/docker-compose.yml— the three service env blocks (kb-server~`:88,mc-server~:210,orchestrator~:308). Each passesNEO_EMBEDDING_PROVIDERand the fullNEO_OPENAI_COMPATIBLE_*set, plusNEO_OLLAMA_KEEP_ALIVE` alone.local-modelservice —ollama/ollama:latest,OLLAMA_HOST=0.0.0.0:11434, reachable on the network aslocal-model:11434, gated behind--profile local-model. The target exists; nothing can be pointed at it.ai/configBase.mjs:654—host: leaf('http://127.0.0.1:11434', 'NEO_OLLAMA_HOST', 'string').The Fix
Pass each leaf through to the services that actually consume it — which is not the same set for all six — and keep every value operator-overridable.
- NEO_OLLAMA_HOST=${NEO_OLLAMA_HOST:-} - NEO_OLLAMA_MODEL=${NEO_OLLAMA_MODEL:-} - NEO_OLLAMA_EMBEDDING_MODEL=${NEO_OLLAMA_EMBEDDING_MODEL:-} - NEO_OLLAMA_EMBEDDING_TIMEOUT_MS=${NEO_OLLAMA_EMBEDDING_TIMEOUT_MS:-}Contract Ledger Matrix
Corrected after @neo-gpt's review on PR #16851, which falsified the original ledger: it specified a file-wide declared-leaf subset while the production claim is per consuming service, and it treated all six leaves as symmetric. The unit of the contract is a
(service, leaf)coordinate.kb-server×HOST,MODEL,EMBEDDING_MODEL,KEEP_ALIVE,EMBEDDING_TIMEOUT_MSdocker-compose.yml${VAR:-}TextEmbeddingService(#getOllamaProvider,#getOllamaEmbeddingTimeoutMs) + the dispatch seam, reachable from the kb entrypointmc-server× the same fivedocker-compose.yml${VAR:-}orchestrator× the same five plusREQUIRE_PARALLEL_MODELSdocker-compose.yml${VAR:-}buildOllamaReadinessConfig, the configured serve task, DreamService, the residency repairkb-server/mc-server×REQUIRE_PARALLEL_MODELSbuildOllamaReadinessConfig; all three callers are underai/daemons/orchestrator/. The module is in both import graphs, but only becauseTextEmbeddingServicedynamic-importsfetchLmsLoadedModelsfrom it — module reachability is not consumptionNEO_OLLAMA_HOSTon a--profile local-modelplane.envhttp://local-model:11434and take effect127.0.0.1:11434leaf default (today's behaviour)Decision Record impact:
aligned-with ADR 0019— the leaves already own env-override-with-default; this is the deployment layer failing to deliver the env, not a config-resolution change. No leaf definitions are touched.Acceptance Criteria
Every declared
NEO_OLLAMA_*leaf reaches each service that consumes it, and no service carries one it does not. Receipt:kb-serverandmc-servercarry 5;orchestratorcarries 6. The original wording of this AC ("all six … to every service") was wrong, and the review that caught the guard caught the ledger with it: shippingREQUIRE_PARALLEL_MODELSto kb/mc advertises an orchestrator knob as theirs.A mechanical check asserts declared-This AC was WRONG and implementing it is what proved so. The invariant is false by design: 8 of 14 declaredNEO_OLLAMA_*-leaves ⊆ compose-passed-vars … the same check applied toNEO_OPENAI_COMPATIBLE_*must pass on the current tree.NEO_OPENAI_COMPATIBLE_*leaves legitimately never reach Compose (BATCH_EMBEDDING_*,CONTENTION_*,UNLOAD_RETRY_*,REQUIRE_PARALLEL_MODELS), because most leaves are tunables whose defaults are authoritative. The required positive control would have failed.Replaced by the invariant that is true and that catches this defect: the contract is a set of
(service, leaf)coordinates. Receipt:test/playwright/unit/ai/deploy/OllamaProviderEnvCoordinates.spec.mjsasserts each required pair independently, requires each value to be a${NAME:-}interpolation of its own variable, and treats aNEO_OLLAMA_*entry with no consumer behind it as a violation rather than a harmless extra.Mutation-convicted, and the mutations are permanent tests rather than a one-off claim —
coordinateViolations()is a pure function of a compose document, so the suite runs it against deliberately broken clones. All four go red: dropping a required leaf from one service; hardcoding the value to a wrong target; interpolating a different variable; adding a coordinate by symmetry. The first two are @neo-gpt's exact-head falsifiers against the predecessor guard, which passed both.Every required coordinate carries a receipt — the consuming module plus the literal source lines that perform the read, matched with whitespace normalised and checked against that service's import graph — so a coordinate cannot be added by assertion alone. Every deliberate absence carries a written reason. A coverage test requires both tables to span the full (service × declared-leaf) product, so a newly declared leaf reddens the suite until its disposition is decided per service.
The weaker family-agnostic floor (a loopback-default leaf must be overridable somewhere in the profile) remains in
LoopbackDefaultReachability.spec.mjs, now stating in its own header that it cannot see service scope and must not be read as "wired correctly".Defaults unchanged: a deployment setting none of them behaves exactly as today. Receipt: every entry is
${VAR:-}, so an unset var yields an empty value and the leaf default stands;417 passedacrossai/deploy/andai/scripts/lint/.On a
--profile local-modelplane withNEO_EMBEDDING_PROVIDER=ollamaandNEO_OLLAMA_HOST=http://local-model:11434, a native embed reaches the model service instead of container-localhost.[L3-deferred — operator handoff needed]— a running compose plane is not reachable from unit CI, so this is carried as Post-Merge Validation on the PR rather than gating merge.Partial receipt already in hand, stated as partial: a CPU-only
ollama/ollama:0.23.1container mounted on the canonicallocal-model-datavolume servedPOST /api/embedforqwen3-embedding—http=200, 4096 dims, runner CPU returning to0.00 %after the call. That proves the model and the wire work in a container. It does not prove the env reacheskb-server, which is the actual claim here and needs the plane.Out of Scope
openAiCompatiblestays the default.#16849(Ollama.stream()timeout),#16846(embedding batch env overrides) — same neighbourhood, separate defects.Avoided Traps
NEO_OLLAMA_KEEP_ALIVEand the fully-implemented provider, readiness-probe and embed paths all say ollama is meant to work. One passed variable out of six is an oversight with a long blast radius, not a decision.NEO_EMBEDDING_PROVIDER=ollama. Whether a given deployment is in that state is a one-command check (docker exec <kb-container> printenv NEO_EMBEDDING_PROVIDER NEO_OLLAMA_HOST), not something this ticket may assume.Related
#16706— deployment-stability epic; the operator runbook gains this as a discriminator.#16830— stuck-runner detection on containerized planes, which probes the same native surface.#16846,#16849— adjacent provider/embedding defects found in the same sweep.Live latest-open sweep: latest 12 open issues plus a scoped title sweep for ollama/compose/env duplicates at 2026-08-10T00:5xZ; no equivalent found.
Structure map: N/A — modifies
ai/deploy/docker-compose.ymlin place.Origin Session ID: d8332b13-5d97-4839-ac11-d2de4602a989
Retrieval Hint:
query_raw_memories("NEO_OLLAMA_HOST not in docker-compose native embed resolves container localhost per-service coordinate guard")