Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ their own. See [RELEASING.md](RELEASING.md).

### Fixed

- **A second OpenAI-compatible, OpenAI or Gemini embedding provider is no longer silently ignored as a fallback** ([#853](https://github.com/goruck/home-generative-agent/issues/853)). Setup builds one embedding client per provider type (per server for Ollama), so a second OpenAI-compatible server listed as an embedding fallback behind an OpenAI-compatible primary was never called; nothing said so. Embedding fallbacks are now kept only as another Ollama server behind an Ollama primary, as the Embeddings feature form already offered: an Ollama server's build of the model is checked against the primary's, but two OpenAI-compatible servers can serve other weights under the same model name, which would mix models in the memory store. A fallback you chose behind a non-Ollama primary now raises the "Embedding fallback is not used" repair issue (open the Embeddings feature and save it to clear it); one HGA added on its own is left out with an info log.
- **A change of embedding model no longer silently breaks memory search** ([#855](https://github.com/goruck/home-generative-agent/issues/855)). HGA checked only the memory store's vector size (#760), so a model change that kept the size (the embedding primary following chat from Ollama to OpenAI, `mxbai-embed-large` swapped for another 1024-dimension model, another build of the same Ollama tag) mixed two models' vectors in the store and memory search returned wrong results with nothing logged. The database now records which model filled the store (provider type, model, size and, for Ollama, the digest of the build's weights); an existing store is recorded as the current model's on the first startup (an Ollama build that the busy startup could not read is read again in the background first, so the check is not skipped). When the embedding model differs from the record, a repair issue names both, and switching back does not clear it, since memories saved in between used the other model. Its fix re-embeds every stored vector with the current model's own provider in the background, then records it: it paces itself so the embedding server keeps time for conversations, resumes after a restart, and brings the issue back if it stops. An Ollama embedding fallback whose server holds another build of the model than the primary's (or, while the primary is down, than the store's record) is left out of the chain, with a repair issue naming it; a kept fallback whose build the busy startup could not read, or the primary's, is checked again in the background and removed from the chain if it holds another build (an embedding call already running on it discards its answer, and no "fallback active" notice is sent), and if it had answered embedding calls meanwhile the record notes it as a writer and the repair is offered; a re-embed running at the time stops and comes back as the repair ([#860](https://github.com/goruck/home-generative-agent/issues/860)). Entries sharing one database must use one embedding model.
- **An embedding fallback no longer mixes models in the memory store** ([#751](https://github.com/goruck/home-generative-agent/issues/751)). The store holds vectors of one model, but when the embedding primary failed, a fallback of another provider type (OpenAI behind Ollama, say) embedded new memories and searches with its own model: memory search returned wrong results when the sizes matched, every write failed when they differed, and it kept using the fallback until a reload. Each provider type embeds with one configured model, so only another Ollama server behind an Ollama primary is now kept as an embedding fallback, and the Embeddings feature form offers only those (OpenAI, Gemini and OpenAI-compatible have one client each, so a second provider of the same type was the primary again, #853). Any other fallback is left out at setup: one you chose raises a repair issue naming it, and one HGA added on its own is left out with an info log. If the embedding primary is down and no fallback answers, even at startup, saving a memory or camera caption now fails until it is back (HGA reloads itself once it answers), instead of being saved without a vector that search never finds again; meanwhile memory search returns the most recent memories and tool retrieval uses keyword matching; chat, camera analysis and summarization fall back across providers as before. Memories saved earlier through a fallback of another model keep that model's vectors; the configuration docs say how to clear them. This also leaves out the Ollama embedding fallback behind a non-Ollama primary that 3.43.4 started building ([#749](https://github.com/goruck/home-generative-agent/issues/749)).
- **An embedding provider of the wrong vector size, and a memory store table that is gone, no longer fail silently** ([#760](https://github.com/goruck/home-generative-agent/issues/760)). The memory store's vector table keeps the size it was first created with, so a non-Ollama provider of another size (an OpenAI-compatible primary at 768 replaced by OpenAI or Gemini at 1024, or an OpenAI-compatible model whose real size differs from its dimensions setting) failed every embedding write: long-term memory search and tool retrieval stopped working, with only a pgvector error in the log. At startup HGA now checks every non-Ollama provider in the Embeddings chain against the store's size (an OpenAI-compatible server is measured with one embedding) and raises a repair issue for a wrong-size primary, and another for wrong-size fallbacks, each with how to fix it. A store table that was gone after setup created it was never created again either: a dropped table (the vector table is the obvious one to drop to change its size), or a Database subentry pointed at a fresh database, failed every write until the integration was removed and added again. Missing tables are now created again at startup; one that cannot be is logged as an error and the integration still loads; a recreated vector table takes the configured size, and memories and camera captions saved before it was dropped stay stored but no longer show up in search. The configuration docs describe how to change the store's size.
- **An embedding fallback no longer mixes models in the memory store** ([#751](https://github.com/goruck/home-generative-agent/issues/751)). The store holds vectors of one model, but when the embedding primary failed, a fallback of another provider type (OpenAI behind Ollama, say) embedded new memories and searches with its own model: memory search returned wrong results when the sizes matched, every write failed when they differed, and it kept using the fallback until a reload. Each provider type embeds with one configured model, so only another Ollama server behind an Ollama primary is now kept as an embedding fallback, and the Embeddings feature form offers only those. Any other fallback is left out at setup: one you chose raises a repair issue naming it, and one HGA added on its own is left out with an info log. If the embedding primary is down and no fallback answers, even at startup, saving a memory or camera caption now fails until it is back (HGA reloads itself once it answers), instead of being saved without a vector that search never finds again; meanwhile memory search returns the most recent memories and tool retrieval uses keyword matching; chat, camera analysis and summarization fall back across providers as before. Memories saved earlier through a fallback of another model keep that model's vectors; the configuration docs say how to clear them. This also leaves out the Ollama embedding fallback behind a non-Ollama primary that 3.43.4 started building ([#749](https://github.com/goruck/home-generative-agent/issues/749)).
- **An embedding provider of the wrong vector size, and a memory store table that is gone, no longer fail silently** ([#760](https://github.com/goruck/home-generative-agent/issues/760)). The memory store's vector table keeps the size it was first created with, so a non-Ollama provider of another size (an OpenAI-compatible primary at 768 replaced by OpenAI or Gemini at 1024, or an OpenAI-compatible model whose real size differs from its dimensions setting) failed every embedding write: long-term memory search and tool retrieval stopped working, with only a pgvector error in the log. At startup HGA now checks a non-Ollama embedding primary against the store's size (an OpenAI-compatible server is measured with one embedding) and raises a repair issue with how to fix it. A store table that was gone after setup created it was never created again either: a dropped table (the vector table is the obvious one to drop to change its size), or a Database subentry pointed at a fresh database, failed every write until the integration was removed and added again. Missing tables are now created again at startup; one that cannot be is logged as an error and the integration still loads; a recreated vector table takes the configured size, and memories and camera captions saved before it was dropped stay stored but no longer show up in search. The configuration docs describe how to change the store's size.
- **A command meant for this house no longer goes to a remote Home Assistant that offers a tool of the same name** ([#843](https://github.com/goruck/home-generative-agent/issues/843)). This Home Assistant and a remote one reached over MCP offer their tools under the same names (`intent__HassTurnOn`, `homeassistant__GetLiveContext`), and the agent can be given one tool per name, so whichever ranked first won: "turn on the porch light" could reach the other house, and a question about this house could be answered from the remote one. A remote Home Assistant's tools now reach the agent named after their server (`upstairs_ha__intent__HassTurnOn`, at most 64 characters), with the server's name in their description, so the agent sees both houses' tools and picks the one you mean; each call is sent to its own house under the tool's own name, including after a PIN confirms it. A remote tool named like one of HGA's own tools no longer shadows it either. Tool exclusions, always-included tools and the tool index keep using the tools' own names. Other MCP servers' tools keep their names.
- **Tool search now reads which house a remote Home Assistant's tool belongs to** ([#848](https://github.com/goruck/home-generative-agent/issues/848)). Since #843 the agent sees both houses' namesake tools, but tool search ranked them on the tool's own name and description, which are the same for both, so at a small tool retrieval limit "turn on the porch light at the Cabin" could retrieve only this house's tool, or only the remote one. A remote Home Assistant's tools are now indexed with its server's name, and the keyword fallback ranks on it too, so naming the house ranks its tools first. Words that do not tell houses apart ("home", "assistant", "house", "server", as in the default "home-assistant" name) do not count. Renaming a remote MCP entry re-indexes its tools on the next request. Give each remote MCP entry a name that says which house it is.
- **A remote Home Assistant's devices can no longer be changed without the Critical Action PIN** ([#840](https://github.com/goruck/home-generative-agent/issues/840)). A remote Home Assistant reached over MCP offers its intents and scripts under the same names as this one (`intent__HassTurnOn`, `script__open_door`), and the PIN check read them as this Home Assistant's own: it looked the targets up here, so `HassTurnOn {"name": "Front Door"}` for a remote garage door matched nothing here and ran with no PIN, and a remote door sharing a name with a light here was checked as the light. Its other tools (a script named by its id alone before 2026.9, `media_player__play_media`) were checked by their arguments only. Now an MCP server offering any tool named like Home Assistant's own intent or script tools, or Home Assistant's `GetLiveContext` or `GetDateTime`, is treated as a remote Home Assistant, judged on every tool it offers, excluded ones included. Every call to it that can change something asks for the PIN, a remote light, list, timer or broadcast included, since the remote devices cannot be seen from here. A call that only reads does not (a state, the weather, the date or time, a temperature, a timer's status, or a tool the server marks read-only), but a script or intent script, and a timer given a command to run later, always do. The call runs with its arguments as given (no lock name filled in here, no alarm-tool redirect), and the PIN prompt names the tool, the arguments it sends as the agent wrote them, and the server (`run HassTurnOff (name Front Door; domain lock) on "Upstairs HA"`). A confirmed PIN request now also goes to the API it was held for, and the same call sent to another one is a new request: before, it went to whatever the reply's turn routed that name to, so a local request could run on a remote Home Assistant offering the same tool name, or the other way round. Other MCP servers are checked by their arguments as before.
Expand Down
13 changes: 13 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,19 @@ label pair ("Server URL" vs "Base URL").

---

### An unmeasured embedding primary loses its wrong-size issue across a reload

**What:** `async_clear_embedding_size_issue` runs on every unload (`async_unload_entry`), so a reload clears `embedding_dims_mismatch` before the new setup's size check runs. If that check cannot measure the primary (an OpenAI-compatible server slow or down at boot, so `_provider_size` returns None), it leaves the issue as it found it, which is now cleared. A known wrong-size primary then fails every embedding write with no repair issue until a later reload measures it.

**Why:** Found by the red-team pass in the #853 review (2026-10-26). It predates #853 (it came with #760); the #853 PR only corrected the docstring to say so. OpenAI and Gemini are unaffected (their size is fixed, never probed).

**How to apply:** Follow `embedding_fallback_other_model` and the other-build issue: clear the size issue only on disable and entry removal, not on a plain unload, so an unmeasured primary keeps the previous setup's verdict. Update `test_unload_drops_the_issue` in `test_embedding_store.py` to expect the issue to survive a reload, and add a test that a disable clears it.

**Effort:** S
**Priority:** P3

---

## Config Entry Lifecycle

### Two Sentinel autonomy options are unreachable, and the docs call them UI-configurable
Expand Down
46 changes: 21 additions & 25 deletions custom_components/home_generative_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@
async_store_tables,
async_sync_embedding_size_issue,
async_sync_other_model_fallback_issue,
embedding_chain_providers,
embedding_size_check_provider,
missing_tables,
)
from .core.face_guard import (
Expand Down Expand Up @@ -2058,8 +2058,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HGAConfigEntry) -> bool:
providers = resolve_model_provider_configs(entry, options)
model_deployments = build_model_deployments(entry, providers, options)
fallback_chains = resolve_fallback_chains(entry, providers, options)
# The store holds one model's vectors, so an embedding fallback of another
# provider type (another model) is left out of the chain (#751).
# The store holds one model's vectors, so an embedding fallback is left
# out of the chain unless it is another Ollama server behind an Ollama
# primary, whose build is checked (#751, #853).
embedding_chain, embedding_left_out = split_embedding_chain(
fallback_chains.get("embedding", [])
)
Expand All @@ -2070,9 +2071,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: HGAConfigEntry) -> bool:
p.name for p in embedding_left_out if p.entry_id in chosen
]
(LOGGER.warning if embedding_left_out_chosen else LOGGER.info)(
"Embedding fallback provider(s) %s left out: they embed with another "
"model than the primary provider %s, which would mix models in the "
"memory store.",
"Embedding fallback provider(s) %s left out: only another Ollama "
"server behind an Ollama primary can be an embedding fallback. Any "
"other fallback of the primary provider %s adds no failover (a "
"second OpenAI or Gemini provider shares its client) or could mix "
"models in the memory store.",
", ".join(p.name for p in embedding_left_out),
embedding_chain[0].name,
)
Expand Down Expand Up @@ -2792,29 +2795,23 @@ def _embedding_instance_for_provider(provider: ModelProviderConfig) -> Any:
)
for p in _emb_fb[1:]:
m = _embedding_instance_for_provider(p)
# One client serves every OpenAI, Gemini or OpenAI-compatible
# provider (#853), and an Ollama one at the primary's URL is the
# primary's: the same client twice is no failover, only a false
# "fell back" and a needless tool index rebuild (#751 review).
# An Ollama fallback at the primary's URL is the primary's client:
# the same client twice is no failover, only a false "fell back"
# and a needless tool index rebuild (#751 review).
if m is None or any(m is c for c, _d, _p in _emb_chain):
continue
# Another build of the model on a fallback server embeds
# differently from the primary's; an unknown digest decides
# nothing (#855).
digest = (
ollama_embedding_digests.get(_ollama_provider_url(p))
if p.provider_type == "ollama"
else None
)
# nothing (#855). Every fallback here is an Ollama server: the
# split above leaves out any other (#853).
digest = ollama_embedding_digests.get(_ollama_provider_url(p))
if builds_differ(digest, reference_embedding_digest):
embedding_other_build.append(p.name)
continue
# Without a database no build was read and there is no store to
# protect, so there is nothing to check later either.
if (
p.provider_type == "ollama"
and db_uri is not None
and (digest is None or primary_embedding_digest is None)
if db_uri is not None and (
digest is None or primary_embedding_digest is None
):
embedding_fallback_unchecked.append(
FallbackBuild(p.entry_id, p.name, _ollama_provider_url(p), digest)
Expand Down Expand Up @@ -2908,11 +2905,10 @@ def _mark_tool_index_stale(
embedding_model = None

# Read by the embedding-size repair check at the end of setup (#760).
embedding_size_providers = embedding_chain_providers(
_emb_fb,
_embedding_instance_for_provider,
embedding_size_provider = embedding_size_check_provider(
_emb_fb[0] if _emb_fb else None,
primary_type=embedding_provider,
primary_embeddings=embedding_model,
primary_embeddings=primary_embeddings,
)

if embedding_model is None and embedding_chain:
Expand Down Expand Up @@ -4670,7 +4666,7 @@ async def _forget_network_audit() -> None:
async_sync_embedding_size_issue(
hass,
entry.entry_id,
providers=embedding_size_providers,
provider=embedding_size_provider,
pool=pool,
),
name="hga_embedding_size_check",
Expand Down
Loading
Loading