All notable changes to Home Mind are documented here.
conversation.processwithout aconversation_idworks now. Home Assistant's integration serializes a missing conversation id as an explicit JSONnull, and the request schema's.optional()covers an absent key but not a null one — so every such call was rejected with a 400 before the model was ever invoked. Voice and the Assist dialog always carry an id and were unaffected; service calls and automations were not. The schema now acceptsnulland treats it as "no conversation in flight" (userIdhad the same latent shape and is covered too), and any client that sends the key conditionally keeps working unchanged.
- Repeat turns are cheaper: the unchanging parts of the prompt are now cacheable. Prompt caching is a prefix match, and the per-request timestamp used to sit ahead of the home layout and device cheat sheet — the largest stable content in the prompt — so providers re-billed them on every turn. The prompt is now ordered least-volatile-first: identity and instructions, then the home description in its own cache block (Anthropic path) or ahead of the volatile tail (plain-text path for OpenAI-compatible endpoints), then timestamps and retrieved facts last. Same content, same answers; on providers with prompt caching the home description is now read from cache on repeat turns, and the saving grows with the size of your home.
276 tests, up from 266.
-
OpenAI's newer models work again. GPT-5-family and o-series models reject
max_tokensoutright —400 Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.— and both OpenAI-backed paths sent the old spelling: the chat engine and the fact extractor. PointingLLM_PROVIDER=openaiat any of those models failed before the model was ever invoked.Switching spellings globally was not an option: every other OpenAI-compatible endpoint Home Mind supports — Ollama, LM Studio, llama.cpp, Azure, gateways — still expects
max_tokens, and being model-agnostic means never breaking the local path to fix the hosted one.Matching on model names would have been wrong rather than merely brittle. The accepted parameter belongs to whoever serves the model, not to the model: a normalising gateway will accept
max_tokensfor a model that rejects it upstream, so the same model id needs different parameters at different base URLs. A name list would also need editing on every OpenAI launch.So
withTokenCap()negotiates instead. It sendsmax_tokens; if the endpoint objects, it sends the request again withmax_completion_tokensand remembers the answer for that model for the life of the process. Endpoints that work today send byte-identical requests. Affected ones pay one rejected request per model per restart — no tokens are billed, since it fails before inference. Retrying is safe on the streaming path because the 400 arrives on the initial request, before any chunk reaches the caller. Detection is deliberately narrow (status 400 withparam: "max_tokens"andcode: "unsupported_parameter", or a message namingmax_completion_tokens), so an unrelated 400 — bad model id, malformed tool schema — still surfaces unchanged.
- A truncated reply on a reasoning model now says what actually happened. On these models the output cap also covers hidden reasoning tokens, so a long deliberation can exhaust the budget before a single visible word is written. The
MAX_TOKENS_TRUNCATEDhint previously blamed prompt size in every case; when we know the endpoint wantedmax_completion_tokens, it now points at reasoning effort instead. The cap itself is unchanged at 2048 (500 for voice).
266 tests, up from 254.
-
Forgetting no longer leaves a tombstone behind. Deleting a fact removed the content, but the turn that did it is itself extractable — "forget that my canary word is bumblebee" / "Forgotten." — so the extractor stored a new fact about the forgetting:
User no longer wants their test canary word remembered,User confirmed deletion of the bedroom cooling automation. Observed live in a real memory store, where they had accumulated quietly and were being read back on recall. A memory asserting that something is not remembered is worse than no memory at all.Two layers, because the prompt alone is not enough on small local models. The extraction prompt now states the rule as NEVER, with three worked BAD examples;
MEMORY_META_PATTERNScatches them deterministically regardless of the model.The pattern is deliberately narrow.
matchesGarbagePattern()is also applied retroactively byMemoryCleanupJob, so anything it matches is deleted from every existing memory store on the next sweep — a loose pattern would silently destroy real facts. Positive verbs (remember/retain/store) only count when explicitly negated, and "asked … to" only counts alongside a delete verb, soUser asked me to remember that 100 ppm is normalsurvives whileYou previously asked me not to retain your canary worddoes not. The test file pins a regression set of twelve real facts taken verbatim from a live store.Existing tombstones are cleared by the cleanup job, which runs 30s after start and every 6h thereafter — no user action needed.
254 tests, up from 233.
- The server now logs which custom prompt is actually in effect. A client can send
customPromptper request and it overrides the server-levelCUSTOM_PROMPT(parsed.data.customPrompt ?? defaultCustomPrompt), but nothing said which one won and the effective prompt was never logged at any level — so a correct value in one place could sit doing nothing while the other silently took precedence, invisible from outside the process.[persona] …now names the source, previews the first 60 characters, and says explicitly when the server-level prompt is being overridden. Logged once and again only when it changes, so it doesn't repeat per message.
Added (llm/tool-definitions.ts, llm/tool-handler.ts, memory/fact-resolution.ts, llm/forget-confirmations.ts)
-
forget_memory— a sixth tool, so a memory can be removed by asking. Until now deletion existed only as REST endpoints (DELETE /api/memory/:userId/facts/:factId), which nobody talking to Assist can reach, so "forget that I like 21°C" had nowhere to go. Now: "forget that my canary word is bumblebee" → the assistant quotes the exact stored fact back and waits → "yes" → deleted.The tool takes a content query, never a fact id: ids are not shown to the model, and tool results are never persisted to conversation history, so an id could not come back on the confirming call anyway. Resolution is deterministic and server-side (
memory/fact-resolution.ts) — exact normalized match first, then Dice similarity over token sets; several close matches return candidates and delete nothing. -
Server-enforced two-call confirmation (
llm/forget-confirmations.ts). The first call only previews; a later call in a different turn commits. This required threading conversation continuity into the tool layer for the first time:handleToolCallnow takes aToolContextcarryingconversationId, a per-turnturnId, theuserIdand the memory store, and both engines create exactly one context per turn.The gate keys on the resolved fact's normalized content, not on the model's wording (which it changes freely between turns) and not on the fact id (the extractor can delete and re-add the same text under a new id in between, which an id-keyed gate would read as a mismatch and re-preview forever). Without conversation continuity — a one-shot API call, say — the tool refuses rather than deleting, because there is nobody to confirm with and a deleted memory does not come back.
-
Post-turn extraction no longer re-learns what was just forgotten. The extractor runs over each turn's transcript, and "forget that my name is Alex" is a transcript containing "my name is Alex" — so the fact reappeared moments later under a fresh id. Extraction now filters against the memories the turn touched.
The filter asks which words changed, not how similar are these, because similarity cannot tell the two cases apart: a replacement ("…canary word is honeybee") and a restatement ("The user's canary word is bumblebee") score identically against the forgotten text. A replacement drops a meaningful word and puts another in its place; a restatement keeps them all. Digits count as meaningful however short, or swapping 21 for 23 reads as no change — and temperatures, times and thresholds are most of what this stores.
- Ported deliberately from the sibling add-on rather than copied: the confirmation gate here is memory-scoped (there are no automations in this project), and the design carries the fixes that project learned in production — content-keyed identity, the numeric-aware filter, refusing without a conversation, and reporting "already forgotten" when the extractor removed the fact before the user confirmed.
- undici 7.28.0 → 7.29.0, clearing five advisories published this week (one high: cross-user information disclosure and parse-time crash via degenerate private cache directives; four moderate: CRLF injection via blob-like body type, cookie attribute injection, cache-control whitespace disclosure, retry-interceptor response desynchronization). undici is the HTTP client used for Home Assistant API calls. Lockfile-only change — the
^7.28.0range already covered it.npm auditreports 0 vulnerabilities.
- Few-shot example names no longer leak into stored memory. The extraction prompt's identity-category examples used real names ("my name is Jure", "I'm also called Hoornet"). Small local models sometimes copy a few-shot example verbatim into real output, so an Ollama-class extractor could store "User's name is Jure" as an actual fact — which then rides into every system prompt via memory injection and resists conversational correction, since nothing the user says outranks a "remembered" fact. Examples now use generic placeholders. Reported against the Nives fork (hoornet/nives#54); the same prompt text existed here.
- Tool-call loops are now bounded. Both chat engines looped on
stop_reason === "tool_use"/finish_reason === "tool_calls"with no iteration cap. A model that gets stuck — re-searching entities, retrying a tool whose result it misreads as a failure — would keep calling until the HA integration's 120-second client timeout, producing nothing and, on a metered API, spending real money to do it. Capped at 8 round-trips (MAX_TOOL_ITERATIONS); the final pass re-issues the request with tool calling disabled (tool_choice: "none"/{type: "none"}) so the model has to answer in words instead of timing out silently.
- Malformed tool arguments no longer fail the whole request.
JSON.parse(tc.function.arguments)ran unguarded inside aPromise.all, so a single truncated or non-JSON argument blob rejected the entire chat request as a 500. Small local models (the common Ollama case) emit these routinely. The parse failure now comes back as a normal tool error the model can recover from on the next turn.
- The periodic fact-cleanup job no longer skips everyone.
getKnownUsers()wasSELECT DISTINCT user_id FROM messages, butcleanupOldConversations()prunes messages older than 24 hours — so any user who hadn't chatted that day disappeared from the list, andMemoryCleanupJoblogged "No known users" and did nothing. Users are now tracked in their own durableuserstable, matching whatInMemoryConversationStorealready did correctly with itsknownUsersset. Existing databases are backfilled from whatever messages remain on first start. AffectsCONVERSATION_STORAGE=sqliteonly.
- Ships the dependency fixes that landed after 0.15.6 but were never tagged: vitest 4.0.18 → 4.1.8 (critical, GHSA-5xrq-8626-4rwp), tsx → ^4.22.4 pulling esbuild 0.28.1 (high-severity advisory), plus
undici,multer,uuid,qs/expressbumps. Anyone installing from the 0.15.6 tag was still on the vulnerable versions. - Additionally clears postcss path-traversal (high, GHSA-r28c-9q8g-f849) and body-parser DoS via silently-disabled size enforcement (GHSA-v422-hmwv-36x6).
npm auditnow reports 0 vulnerabilities.
- Stale "HomeMind PRO" strings corrected to "Home Mind" (OpenRouter
X-Title/referer, usage-limit notification), and the usage-limit copy reframed for OSS/BYOK rather than a paid service. Easy Install section now points at the Nives add-on instead of a dead repo URL.
localMidnightIsofield onformatDateTimeWithOffset()'s return shape. The dynamic prompt block now includes aLocal midnight today (UTC)line with an unambiguous ISO timestamp the LLM should use asstart_timefor "today's X" history queries. Previously the model would infer "today" from the date string and end up sending2026-05-13T00:00:00Z— which is midnight UTC, not midnight local. For CEST that meant skipping the first 2 hours of the local day; for EST it would pull 5 hours of yesterday into "today". The injected value is computed from the runtime's local TZ, so it works correctly for every offset (including half-hour ones like UTC+5:30).
- Tightened the "when did X start today?" rule to a prescriptive universal principle. Previously the prompt said "the first non-zero reading is usually pre-dawn sensor noise; pick when the value crosses a meaningful threshold OR describe the ramp" — which models interpreted as advisory and often did both (correctly describing the ramp and still naming the first non-zero datapoint). The new wording is:
NEVER report the first non-zero datapoint as the start time. It is almost always idle current, sensor noise, or a recorder artifact — not real activity. Either find when the value first crossed ~10% of today's peak observed value, or describe the ramp shape without naming a specific start. The data's own shape — not absolute clock times — defines when something meaningfully started.
- This generalizes to any rate/power/flow sensor (solar inverters, water meters, motion-cumulative, miners, HVAC, etc.) and avoids latitude/season-specific clock-time hardcoding.
Two distinct bugs surfaced in the same real-HA query: (1) the model querying with midnight UTC instead of local midnight, and (2) reporting an inverter idle reading at 4:14 AM as "solar started at 4:14 AM" despite the previous "describe the ramp" advisory. Fix #1 is structural (the model can no longer get the local-day boundary wrong). Fix #2 is prescriptive (the model cannot interpret "describe the ramp" as additive to naming a pre-dawn timestamp).
- "Entity discovery — don't give up before searching" section instructs the model to try
search_entitieswith relevant keywords (system word, brand, domain, room name, device type) before declining to answer. Addresses a real-HA pattern observed during cheap-tier piloting where models would say "I don't have that tool" for things like solar production rather than searching for the entity first. - "Today's X / past-data queries" section gives explicit guidance on two failure shapes:
- For daily totals, use
get_historyover today's range — not the instantaneous current state of asensor.*_current_powerentity. - For "when did X start today?" on noisy sensors (solar inverters, motion-cumulative, water meters), the first non-zero reading is often pre-dawn sensor noise or idle current. Report when the value crosses a meaningful threshold, or describe the ramp in plain language.
- For daily totals, use
Both new sections appear in SYSTEM_INSTRUCTIONS (full) and VOICE_INSTRUCTIONS (compact) in src/llm/prompts.ts. Total cost: ~250 extra tokens per conversation (cached via prompt caching where supported).
Observed failures during real-HA cheap-model piloting: (a) Mistral-Small declining solar production questions without trying search_entities; (b) cheap models reporting "0 produced today" because they read instantaneous power instead of daily history; (c) the "4:13 AM solar start" mistake (84W pre-dawn reading reported as the day's start). All three are addressable on the prompt side and lift any model running through Home Mind, not just one.
OPENAI_RESPONSE_FORMATenv var (closes #21). When set tojson_object, the OpenAI-compatible fact extractor sendsresponse_format: { type: "json_object" }on every extraction call. Required by some OpenAI-compatible providers (notablyqwen3.6:27bvia Ollama, per @rgnyldz's report) that otherwise return empty content. Unset by default → behaviour unchanged for providers that don't need it.OPENAI_MAX_TOKENSenv var. Override the fact extractor'smax_tokens(default1000). Only affects the extractor — chat keeps itsisVoice-based defaults (500/2048). Useful when a local model truncates JSON output at the default ceiling. Thanks to @rgnyldz for surfacing the underlying need.- Structured
errorfield onChatResponse— when chat returns no text and no tool calls, the server now emits{ code, hint }instead of a silent empty response. Codes:EMPTY_CONTENT,MAX_TOKENS_TRUNCATED,CONTENT_FILTERED. - HA integration surfaces
error.hintdirectly to the user (src/ha-integration/custom_components/home_mind/conversation.py). The previous fallback string — "I received your request but got no response." — was indistinguishable across very different failure modes (model returned nothing, max_tokens truncation, content-filter block, shim/proxy returning non-streaming responses). Users now get a specific hint about what to check, surfaced directly in HA Assist.
The generic fallback string had been around since the first integration, and it hit at least two distinct failure modes we know of: @rgnyldz's qwen3.6:27b case (missing response_format hint) and our own FunctionGemma testing (shim returning non-streaming JSON instead of OpenAI SSE chunks). Both surfaced the same opaque message in HA Assist, making the problem far harder to diagnose than it needed to be. The shape now distinguishes failure modes and surfaces fixable hints to the user.
get_historytool calls with explicit timezone offsets no longer 400. When the LLM passedstart_time/end_timecontaining a+HH:MMoffset (e.g.2026-05-11T09:46:47+02:00), the+was interpolated raw into the request URL. HA's HTTP layer (aiohttp) decodes+as space in query strings, so the timestamp arrived as2026-05-11T09:46:47 02:00and HA rejected it with400: Invalid end_time. Any model smart enough to include its local TZ hit this — observed first withmistralai/mistral-small-3.2-24b-instructretrying 8+ times before giving up. All interpolated values ingetHistory()are now run throughencodeURIComponent. Regression test added insrc/ha/client.test.ts. Thanks @hoornet (real-HA pilot) for the smoking-gun log.
- Refreshed npm dependencies to pick up upstream security patches.
npm audit fixliftedmulter(CVE: DoS via uncontrolled recursion / resource exhaustion / incomplete cleanup),undici(CVE: WebSocket parser overflow, CRLF injection, request smuggling, unbounded WebSocket/dedup memory consumption, server_max_window_bits validation),path-to-regexp(ReDoS, transitive via express), andqs(arrayLimit bypass, transitive via express) inside their existing semver ranges. Dev-only chains (vite,postcss,rollup,picomatchvia vitest) were updated in the same pass. Nopackage.jsonedits, no API or behaviour changes — pull this if you build the server locally and want a cleannpm audit.
- More forgiving fact-extractor JSON parsing. Some OpenAI-compatible models (notably
qwen3.6:27b, but also a few Phi/Gemma variants and the occasionalgpt-4o-miniresponse) emit either a single JSON object instead of a[...]array, or append trailing prose after the JSON. StrictJSON.parse+Array.isArraywould drop every fact in those cases — silently, since extraction runs fire-and-forget. The extractor now (a) wraps a single object into a one-element array, and (b) falls back to a regex slice (/\[[\s\S]*\]/) when the raw response isn't pure JSON. Failures still log a one-line warning with the first 200 chars of the raw response so it's diagnosable fromLOG_LEVEL=debug. All existing input shapes parse identically — the change is strictly additive. Thanks to @rgnyldz (#20) for the diagnosis and the diff that motivated this fix.
- Recall now reliably retrieves stored facts. The chat path previously used Shodh's
proactive_context(graph-based spreading activation) as the sole retrieval source when a user message was present. When activation didn't fire — typos in the query, cold memories, weak semantic links — facts were omitted from the system prompt and the LLM replied "I don't know" even though the fact was visible underGET /api/memory/{userId}. Retrieval now always pulls the user's tagged fact set via/api/recall/tagsand, when a query is provided, merges inproactive_contextresults at the front as a relevance boost. Deduplicated by id, trimmed to token budget. Ifproactive_contextfails, tag recall still delivers facts.
MEMORY_TOKEN_LIMITdefault raised from 1500 → 3000. The static part of the system prompt is already cached via Anthropic prompt caching, so a larger fact budget costs essentially nothing on the hot path. Users with many memories now get more of them into context by default.
[recall]debug log — whenLOG_LEVEL=debug, each chat turn logsuserId, fact count, and approximate token usage. Useful for diagnosing recall issues (e.g. distinguishing "facts weren't retrieved" from "LLM ignored the facts").
- Auto-detect user language — server detects the language of the user's message and responds in the same language without any configuration. Works across all LLM providers.
- OpenRouter attribution headers — when routing through OpenRouter, requests include
HTTP-RefererandX-Titleheaders for proper attribution and usage tracking in the OpenRouter dashboard.
- Shodh Memory updated to v0.1.91 — switched to the official
varunshodh/shodh-memory:latestDocker image (0.1.80 → 0.1.91). Includes SHA-256 content deduplication, improved graph memory, and stability fixes. - Official Shodh Docker image — the custom Shodh Dockerfile is now a thin wrapper around the official image with a migration entrypoint for volume permissions. No more manual binary/library management.
- Non-root container volume paths — corrected volume mount paths so the server runs correctly as a non-root user inside Docker.
- Docker security hardening — containers run with restricted capabilities, read-only root filesystem where possible, and tightened auth token comparison.
- Home Layout Index — server now queries the HA template API with Jinja2 functions (
floors(),floor_areas(),area_entities(), etc.) at startup (and every 30 min) and injects a compact floor→room→entity map into every system prompt. The LLM knows which floor and room each device belongs to without tool calls or guessing. Fixes cases where the LLM incorrectly assumed spatial location of devices (e.g. radiators on the wrong floor). Gracefully degrades if template API is unavailable (older HA) or floors/areas aren't configured. - Server-side TTS —
POST /api/ttsendpoint backed by OpenAI TTS API (or any compatible endpoint). Returnsaudio/mpeg. Configured viaTTS_PROVIDER,TTS_API_KEY,TTS_BASE_URL,TTS_MODEL,TTS_VOICE. Returns 501 when disabled. - Admin conversations endpoint —
GET /api/admin/conversationsreturns all known users and their conversation summaries in one call. Useful for reviewing stored conversations without screenshots. Auth-protected via existing bearer token middleware. - Device Capability Index — server scans all
light.*entities at startup and builds a per-entity capability cheat sheet injected into every system prompt. The LLM reads exact color control params (e.g.rgbw_color,color_temp_kelvin,xy_color) directly from the cheat sheet rather than re-discovering them via tool calls on each request. This eliminates repeatedsearch_entities/get_entitiescalls for known devices and prevents wrong color params on first attempt. DEVICE_OVERRIDESenv var — JSON map of per-entity capability overrides for devices whose HA-reported modes don't match their actual wiring (e.g. Gledopto GL-C-008P wired as RGB-only but firmware always reportscolor_temp+xy). Example:DEVICE_OVERRIDES={"light.gledopto_gl_c_008p": {"whiteMethod": "rgb_white"}}. See README for details.
- LLM tool narration — LLM was outputting "Let me search...", "I found...", "Done!" text between tool calls which got concatenated into messy responses. Added explicit no-narration rule to both chat and voice prompt variants.
- WLED RGBW white light — scanner correctly detects
rgbw/rgbwwmodes and tells the LLM to usergbw_color: [0,0,0,255](dedicated W channel) even whencolor_tempis also listed — WLED reports it but ignores it.
- Markdown rendering — assistant messages now render bold, lists, code blocks, headings etc. via
react-markdown+@tailwindcss/typography. User messages stay as plain text. - Persistent login — app auto-configures on first install without showing the setup screen. Set
VITE_DEFAULT_SERVER_URL(and optionallyVITE_DEFAULT_API_TOKEN) in.env.localbefore building. Settings are still saved tolocalStorageafter first load, so subsequent installs on the same device skip setup entirely.
- Persistent conversation history — SQLite-backed conversation storage that survives server restarts. Set
CONVERSATION_STORAGE=sqliteto enable. Max 20 messages per conversation with automatic trimming. In-memory mode remains the default. - Memory cleanup job — runs every 6 hours, removes low-confidence facts and common LLM extraction artifacts (transient states, too-short facts). Pattern-based filtering in
fact-patterns.ts.
wiki-drafts/directory — wiki is now published on GitHub
- Shodh forget endpoint —
DELETE /api/forget/{memory_id}?user_id=...instead ofPOST /api/forgetwhich returned 404. Fixes fact deletion, bulk clear, and fact replacement during extraction. Workaround for shodh-memory#33.
- Proactive context crash —
/api/proactive_contextreturns flat memory objects (noexperiencewrapper) unlike other Shodh endpoints.toFact()now handles both response shapes. - History token overflow —
get_historyfor sensors with frequent state changes (e.g. temperature over 2 days) could return thousands of entries exceeding the 200K token limit. Now strips attributes and downsamples to 200 entries max.
- Fact extraction quality — rewrote extraction prompt with explicit DO NOT rules and bad examples. LLMs previously stored garbage like transient device states ("light is currently red"), assistant actions, and single-event troubleshooting observations. New prompt includes confidence scoring and "if in doubt, return []" rule.
- Post-extraction filtering — code-level safety net rejects facts that are too short (<10 chars), contain transient-state patterns ("currently", "right now", "was just"), or have low confidence (<0.5). Skipped facts logged at debug level with
[filter]prefix. - Batch fact storage — extracted facts are now stored in a single Shodh batch call (
/api/remember/batch) instead of N individual calls, reducing latency. - Proactive context retrieval — uses Shodh's graph-based spreading activation (
/api/proactive_context) instead of plain semantic search, so co-accessed memories activate each other. - Tag-based fact recall —
getFacts()now uses Shodh's/api/recall/tagsendpoint (filtering byhome-mindtag) instead of semantic-searching for the literal string "all memories".
confidencefield (0.0–1.0) on extracted factsaddFacts()batch method onIMemoryStoreinterfacerememberBatch(),recallByTags(),getProactiveContext()methods onShodhMemoryClient
- Single fact deletion after restart —
DELETE /api/memory/:userId/facts/:factIdreturned 404 after server restart becausedeleteFact()relied on an in-memory map to look up userId. Now userId is passed directly from the route parameter, so deletes work reliably regardless of server restarts. Also fixes fact replacement during extraction (thereplacesfield in extracted facts).
- Ollama provider support — run Home Mind with local LLMs, no API key needed
- Set
LLM_PROVIDER=ollamaandLLM_MODEL=<model>(e.g.,llama3.1,qwen2.5) - Optional
OLLAMA_BASE_URLfor non-default endpoints - Reuses the OpenAI-compatible chat engine (Ollama exposes an OpenAI API)
OLLAMA_BASE_URLpassed through Docker Compose for containerized setups
- Set
- This completes multi-LLM provider support: Anthropic, OpenAI, and Ollama (GitHub issue #1)
- White light on RGBW strips — use
rgbw_color: [0,0,0,255](dedicated W channel) instead ofcolor_temp_kelvinwhich WLED doesn't render correctly - White light on RGB-only lights — use
rgb_color: [255,255,255]for lights that lack a white channel (e.g., Gledopto GL-C-008P).color_temp_kelvinis accepted by HA but doesn't work on RGB-only controllers - Enriched light tool descriptions —
call_servicetool now documentsbrightness,rgb_color,color_temp_kelvin,hs_color, andrgbw_colorfields with usage guidance, so the LLM picks the right color mode (GitHub issue #13) - History timezone mismatch — bare ISO timestamps from the LLM (no timezone suffix) are now normalized with
Zbefore passing to HA, preventing empty history results
- Custom system prompt — customize AI personality and behavior
- Server-level default via
CUSTOM_PROMPTenv var - Per-request override via
customPromptfield in chat API payload - HA integration options flow for configuring custom prompt in the UI
- Request-level prompt takes precedence over server-level default
- Server-level default via
- CHANGELOG.md — version history for users tracking updates
- Auto-generated SHODH_API_KEY — deploy script now generates the Shodh API key automatically if not set, removing a manual setup step
SHODH_API_KEYremoved from required env vars in installation docs — users no longer need to generate it manually.env.examplenow ships withSHODH_API_KEYcommented out
- Multi-LLM provider support — use OpenAI as an alternative to Anthropic
- Set
LLM_PROVIDER=openaiandOPENAI_API_KEYto switch providers - Optional
OPENAI_BASE_URLfor Azure or local proxy endpoints
- Set
- Provider-neutral tool system — HA tools work identically across providers
- LLM interfaces extracted (
IChatEngine,IFactExtractor) with per-provider implementations - Factory pattern (
llm/factory.ts) selects provider at startup based onLLM_PROVIDERenv var - Default model updated from Claude Haiku 3.5 to Claude Haiku 4.5
- Empty env vars (from Docker Compose) now correctly treated as undefined for optional config fields
- Extracted LLM interfaces in preparation for multi-provider support (Phase 1 refactor, no user-facing changes)
- Public release with installation guide and HACS integration
- Docker Compose deployment with health checks
- Shodh port 3030 exposed for direct TUI access
- Comprehensive test suite
- Troubleshooting documentation
- Renamed ha-bridge to home-mind-server
- Consolidated to single architecture with Shodh as the only memory backend (removed SQLite fallback)
- Dynamic version reading from package.json
- Shodh container now uses Ubuntu 24.04 (GLIBC 2.38+ requirement)
- ONNX runtime bundled with Shodh to avoid download failures
- Shodh client retry logic and increased timeouts
- Shodh updated to v0.1.75
- Architecture consolidation — single path through Shodh Memory, no fallback stores
- Renamed ha-bridge to home-mind-server
- Shodh Memory integration for cognitive memory with semantic search
- Comprehensive memory store tests
- Anthropic prompt caching for faster responses (static system prompt cached via
cache_control: ephemeral) - Smart fact replacement — new facts automatically supersede conflicting old ones via
replacesfield
- Conversation history for multi-turn voice interactions (in-memory, max 20 messages per conversation)
- Project renamed to Home Mind
- Voice assistant working via HA Assist (Wyoming protocol)
- Streaming responses (SSE) for faster perceived response times
- Text Assist with live sensor data
- Voice prompt tuned to match quality of web prompt
- Initial HA Bridge API server and HA custom component
- Home Assistant tool calls (
get_state,get_entities,search_entities,call_service,get_history) - Memory extraction from conversations (fact categories: baseline, preference, identity, device, pattern, correction)
- Sensor history querying