From 308b8a4d1f3c768168e5c85c1d83cebcc99cdb15 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:29:41 +0200 Subject: [PATCH] fix: bound broad tool workflows --- compose.yaml | 2 +- dev/test_openwebui_filters.py | 43 ++++++++++--- docs/CURRENT_REFERENCE.md | 13 +++- docs/TOOLING_RELIABILITY_2026-08-24.md | 19 +++++- docs/TOOL_ARCHITECTURE_2026-08-24.md | 17 ++++- platform/mcp/README.md | 2 +- platform/mcp/compose.yaml | 4 +- platform/mcp/install-tools.sh | 2 +- .../openwebui/filters/auto_tool_selector.py | 43 ++++++++++--- platform/openwebui/patch_tool_finalization.py | 64 +++++++++++++++++++ platform/operator/install-operator.sh | 6 +- 11 files changed, 186 insertions(+), 29 deletions(-) diff --git a/compose.yaml b/compose.yaml index 62f044f..01e7eed 100644 --- a/compose.yaml +++ b/compose.yaml @@ -714,7 +714,7 @@ services: build: context: . dockerfile: platform/openwebui/Dockerfile - image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v6} + image: ${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-agent-loop-v7} container_name: mike-ai-open-webui restart: unless-stopped volumes: diff --git a/dev/test_openwebui_filters.py b/dev/test_openwebui_filters.py index df8e7a9..389e7b9 100644 --- a/dev/test_openwebui_filters.py +++ b/dev/test_openwebui_filters.py @@ -212,6 +212,14 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): self.assertEqual( result["tool_ids"], ["server:mcp:homeassistant-local"] ) + self.assertIn("Never call ha_list_states merely", result["messages"][0]["content"]) + self.assertIn("Never infer an automation entity_id", result["messages"][0]["content"]) + + async def test_hyphenated_homeassistant_and_tool_name_are_selected(self): + result = await self._select( + "Führe einen Home-Assistant-Test aus und nutze ha_list_states genau einmal." + ) + self.assertEqual(result["tool_ids"], ["server:mcp:homeassistant-local"]) async def test_general_web_is_available_without_site_specific_rules(self): result = await self._select("Erkläre mir kurz, wie ein Fahrrad funktioniert.") @@ -219,15 +227,15 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(result["metadata"]["features"]["web_search"]) self.assertNotIn("tool_ids", result) - async def test_weather_uses_native_web_without_mcp(self): + async def test_weather_gets_native_web_and_compact_fallback(self): result = await self._select("Soll es heute in Rastatt regnen?") - self.assertNotIn("tool_ids", result) + self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"]) - async def test_youtube_channel_question_uses_native_web(self): + async def test_youtube_channel_question_gets_general_web_fallback(self): result = await self._select( "Welches Video steht aktuell oben auf dem YouTube-Kanal The Proper People?" ) - self.assertNotIn("tool_ids", result) + self.assertEqual(result["tool_ids"], ["server:mcp:web-general-local"]) async def test_unraid_uses_readonly_not_mua(self): result = await self._select( @@ -272,7 +280,10 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): "aktuelle offizielle Liste online und prüfe jede fehlende Folge bei Deezer. " "Nur lesen, ohne Downloads oder Änderungen." ) - self.assertEqual(result["tool_ids"], ["server:mcp:mua-readonly-local"]) + self.assertEqual( + result["tool_ids"], + ["server:mcp:mua-readonly-local", "server:mcp:web-general-local"], + ) self.assertTrue(result["features"]["web_search"]) self.assertTrue(result["metadata"]["features"]["web_search"]) self.assertIn("search_web/fetch_url", result["messages"][0]["content"]) @@ -329,20 +340,36 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): ) self.assertIn("integrate or relay", result["messages"][0]["content"]) - async def test_github_and_explicit_web_only_adds_github_mcp(self): + async def test_readonly_integration_plan_does_not_attach_unraid_admin(self): + result = await self._select( + "Analysiere https://github.com/foo/deemix und prüfe den auf Unraid " + "laufenden Container und entwirf einen Athena-MCP. Erstelle keinen zweiten Container, ändere nichts " + "und lies keine Secrets." + ) + self.assertEqual( + result["tool_ids"], + [ + "server:mcp:github-local", + "server:mcp:athena-operator-local", + "server:mcp:mua-readonly-local", + ], + ) + + async def test_github_and_explicit_web_adds_compact_general_web(self): result = await self._select( "Prüfe dieses GitHub Repository und suche zusätzlich im Netz nach Nutzerstimmen." ) self.assertEqual( result["tool_ids"], - ["server:mcp:github-local"], + ["server:mcp:github-local", "server:mcp:web-general-local"], ) - async def test_plain_public_web_request_does_not_attach_legacy_web_mcp(self): + async def test_plain_public_web_request_attaches_general_not_legacy_web(self): result = await self._select( "Suche im Netz auf MakerWorld einen Schlümpfe-Schlüsselanhänger." ) self.assertNotIn("server:mcp:web-local", result.get("tool_ids", [])) + self.assertIn("server:mcp:web-general-local", result.get("tool_ids", [])) async def test_manual_tool_is_preserved(self): result = await self._select( diff --git a/docs/CURRENT_REFERENCE.md b/docs/CURRENT_REFERENCE.md index 4f3b7fd..796eefb 100644 --- a/docs/CURRENT_REFERENCE.md +++ b/docs/CURRENT_REFERENCE.md @@ -147,7 +147,7 @@ Der isolierte Eignungs- und Ausfalltest ist in - Docker Compose - SearXNG, per Digest gepinnt -- TinySearch 0.5.1, per Digest gepinnt +- TinySearch 0.6.1, per Digest gepinnt - TinySearch ausschließlich im internen Docker-Netz, ohne Host-Port - lokale ONNX-Embeddings - OpenWebUI-native allgemeine Suche und Seitenabruf in allen normalen Profilen @@ -209,7 +209,16 @@ höchstens zwölf Aufrufe desselben Werkzeugnamens und höchstens zweimal exakt dieselbe Signatur. Nach Ende des Budgets stehen zusätzliche interne Runden ausschließlich für eine sichtbare werkzeugfreie Schlussantwort bereit. Das produktive OpenWebUI-Derivat trägt den Tag -`mike-ai/openwebui:main-01f4282-agent-loop-v6`. +`mike-ai/openwebui:main-01f4282-agent-loop-v7`. V7 begrenzt zusätzlich die +Werkzeugantworten in OpenWebUIs internen Fortsetzungsrunden auf 12.000 Zeichen +je Ergebnis und 64.000 Zeichen pro Antwortlauf. Damit greift die Begrenzung +auch bei Ergebnissen, die erst nach dem ersten Request entstehen. + +Auto Tool Selector 4.2 hält native Websuche immer verfügbar, ergänzt bei +expliziter Webrecherche den TinySearch-Fallback und erkennt unter anderem +`Home Assistant`, `Home-Assistant` sowie direkte `ha_*`-Werkzeugbezüge. Für +Home Assistant verlangt er gezielte Zustandsabfragen und verbietet die +Ableitung einer `entity_id` aus einer YAML-`id`. Der Platform Context MCP läuft ohne Docker-Socket, Shell, Egress oder Secrets. Ein root-eigener Minutentimer erzeugt nur einen begrenzten Laufzeitsnapshot. diff --git a/docs/TOOLING_RELIABILITY_2026-08-24.md b/docs/TOOLING_RELIABILITY_2026-08-24.md index ed029c2..21af403 100644 --- a/docs/TOOLING_RELIABILITY_2026-08-24.md +++ b/docs/TOOLING_RELIABILITY_2026-08-24.md @@ -39,8 +39,11 @@ benötigte deshalb kleinere, klarere Werkzeuge und harte Abbruchgrenzen. erhält der Benutzer deshalb eine sichtbare Antwort aus den vorhandenen Befunden samt ehrlicher Angabe fehlender Belege. Inlet-Filter allein können dies nicht erzwingen, weil sie zwischen OpenWebUIs internen Werkzeugrunden - nicht erneut ausgeführt werden. Ein einzelnes Resultat ist auf 12.000, alle - Resultate zusammen auf 64.000 Zeichen begrenzt. + nicht erneut ausgeführt werden. Seit OpenWebUI-Derivat V7 sitzt die + Größenbegrenzung deshalb direkt in der internen Fortsetzungsschleife: ein + einzelnes Resultat ist auf 12.000, alle Resultate zusammen auf 64.000 + Zeichen begrenzt. Zitate und sichtbarer Werkzeugstatus werden zuvor + verarbeitet; das Modell erhält eine markierte Kopf-/Ende-Verdichtung. Die Basis ist unveränderlich auf OpenWebUI-Revision `01f4282f1ffe0d6212f58d3afbeae21fffd0c4be` beziehungsweise Image-Digest `sha256:6a773e5c3a246b65cbe74ce942b294292c0e5f81c138f703d111bc162f7d7c3d` @@ -65,7 +68,7 @@ benötigte deshalb kleinere, klarere Werkzeuge und harte Abbruchgrenzen. ## Abnahme -- OpenWebUI-Filtertests: 34 +- OpenWebUI-Filtertests: 36 - Web-MCP-Tests: 9 - Athena-Operator-Tests: 13 - Platform-Context-Test: bestanden @@ -82,6 +85,16 @@ sudo /opt/mike-ai/stack/dev/verify_mcp_catalogs.sh Er muss mit `MCP_CATALOG_SUITE_OK` enden. +## Reale Browserabnahme + +Die angemeldete OpenWebUI-Sitzung bestand am 24. August 2026 folgende Läufe: + +- unbekannte Website MakerWorld über native Suche plus TinySearch 0.6.1 +- GitHub-Repository plus vorhandener Unraid-Container plus Athena-Planung +- mehrstufige Home-Assistant-YAML-Analyse ohne Vollinventar +- bewusst angeforderte 223-KB-Klasse einer Home-Assistant-Zustandsliste; V7 + lieferte trotz 2.169 Zuständen nach 30,9 Sekunden eine sichtbare Kurzantwort + ## Noch manuell zu prüfen Ein echter Browsertest mit einer bewusst synthetischen CSV benötigt eine diff --git a/docs/TOOL_ARCHITECTURE_2026-08-24.md b/docs/TOOL_ARCHITECTURE_2026-08-24.md index a62550f..b48a7df 100644 --- a/docs/TOOL_ARCHITECTURE_2026-08-24.md +++ b/docs/TOOL_ARCHITECTURE_2026-08-24.md @@ -43,7 +43,9 @@ MCPs sind über feste WireGuard-Ports auch für Hermes und Pi erreichbar. - bei ausgeschöpftem Budget folgt zwingend eine werkzeugfreie, sichtbare Schlussantwort aus den bereits erhobenen Befunden - Werkzeugausgaben bleiben kurz: 12.000 Zeichen je Ergebnis und 64.000 Zeichen - über den Verlauf; ältere Resultate werden zuerst verdichtet + über den Verlauf. Die Begrenzung sitzt in OpenWebUIs interner + Fortsetzungsschleife und greift daher auch auf Resultate, die erst nach dem + ersten Modellschritt entstehen. Damit stoppt die Plattform bewiesene Schleifen, nicht normale lange Recherche. Die früheren Grenzen von zwölf Gesamtaufrufen und vier Aufrufen je Werkzeug @@ -54,7 +56,7 @@ waren für Qwen3.8-Agentenaufgaben zu klein. | Client | Standardweg | |---|---| | OpenWebUI | native `search_web` und `fetch_url`, immer verfügbar | -| Hermes/Pi/andere MCP-Clients | `http://192.168.1.212:8203/mcp` (TinySearch) | +| Hermes/Pi/andere MCP-Clients | `http://192.168.1.212:8203/mcp` (TinySearch; laufender Alt-Gateway zusätzlich 8211 bis zum nächsten geplanten WireGuard-Neustart) | | Spezial-/Rollbackbedarf | historischer `mcp-web` nur mit Compose-Profil `legacy-web` | TinySearch stellt die vier Upstream-Werkzeuge `search`, `scrape_urls`, @@ -81,3 +83,14 @@ Nach Änderungen müssen mindestens folgende Prüfungen erfolgreich sein: 6. Browserlauf mit einer unbekannten öffentlichen Website, GitHub plus Laufzeitprüfung sowie einer mehrstufigen Home-/Unraid-Aufgabe +Produktive Abnahme am 24. August 2026: + +- MakerWorld ohne Site-Adapter: 12 Werkzeugaufrufe, verifizierter Treffer mit + Downloadzahl und Direktlink, sichtbare Antwort nach 102,5 Sekunden +- GitHub + Unraid + Athena: 12 gezielte Repository-Leseaufrufe plus + Laufzeitprüfung; vorhandenen Deemix-Backendcontainer korrekt wiederverwendet +- Home Assistant YAML: drei auskommentierte Automatisierungen gefunden, ohne + vollständige Zustandsliste und mit sichtbarer Abschlussantwort nach 88,0 Sekunden +- absichtlicher Großausgabetest: 2.169 Home-Assistant-Zustände in einem + Werkzeugresultat; durch V7 intern verdichtet und nach 30,9 Sekunden korrekt + mit ausschließlich Anzahl und Testsatz beantwortet diff --git a/platform/mcp/README.md b/platform/mcp/README.md index c247d76..8fbac0d 100644 --- a/platform/mcp/README.md +++ b/platform/mcp/README.md @@ -64,7 +64,7 @@ TinySearch bleibt als Ganzes read-only. Nur das flüchtige tmpfs-Verzeichnis temporären Browser- und Sitzungszustand erzeugt. Es wird bei jedem Container-Neustart vollständig verworfen. -TinySearch ist der allgemeine portable Web-MCP. Auf VPN-Port 8203 können Hermes, +TinySearch 0.6.1 ist der allgemeine portable Web-MCP. Auf VPN-Port 8203 können Hermes, Pi und andere Clients seine vier Upstream-Werkzeuge direkt nutzen. SearXNG ist der Such-Backenddienst. Die historische eigene Web-Fassade ist nur Rollback. diff --git a/platform/mcp/compose.yaml b/platform/mcp/compose.yaml index dc8255d..eac8717 100644 --- a/platform/mcp/compose.yaml +++ b/platform/mcp/compose.yaml @@ -56,7 +56,9 @@ services: tinysearch: <<: *tool-common - image: marcellm01/tinysearch@sha256:5a03d5a1f1b0fabe48f2a26e05db4a84bcb611106a57ec51e42db4549976aa9c + # TinySearch v0.6.1. This release fixes the crawler behavior observed with + # v0.5.1 and adds the current search -> scrape_urls workflow. + image: marcellm01/tinysearch@sha256:7a7d0585f5000f462e699e42b97409715826a9e2edcd09a166afa93a4b7cba31 container_name: mike-ai-tools-tinysearch dns: ["${AI_DNS:-1.1.1.1}"] # Crawl4AI keeps transient browser/session state here. The container stays diff --git a/platform/mcp/install-tools.sh b/platform/mcp/install-tools.sh index 673386a..6e84ed1 100755 --- a/platform/mcp/install-tools.sh +++ b/platform/mcp/install-tools.sh @@ -70,7 +70,7 @@ fi # redundant download can hang indefinitely. Prepare only the persistent ONNX # bundle that is actually absent on a fresh installation. docker volume create mike-ai-tools_tinysearch-models >/dev/null -tiny_image="marcellm01/tinysearch@sha256:5a03d5a1f1b0fabe48f2a26e05db4a84bcb611106a57ec51e42db4549976aa9c" +tiny_image="marcellm01/tinysearch@sha256:7a7d0585f5000f462e699e42b97409715826a9e2edcd09a166afa93a4b7cba31" if ! docker run --rm --entrypoint test \ -v mike-ai-tools_tinysearch-models:/data/models "$tiny_image" \ -f /data/models/all-minilm-l6-v2-onnx/model.onnx; then diff --git a/platform/openwebui/filters/auto_tool_selector.py b/platform/openwebui/filters/auto_tool_selector.py index 971fd86..4351247 100644 --- a/platform/openwebui/filters/auto_tool_selector.py +++ b/platform/openwebui/filters/auto_tool_selector.py @@ -1,7 +1,7 @@ """ title: MikeAI Auto Tool Selector author: MikeAI -version: 4.0.0 +version: 4.2.0 description: Keeps broad web access available and adds relevant portable MCP domains without acting as a gate. """ @@ -31,6 +31,7 @@ class Filter: "navidrome": "server:mcp:navidrome-local", "platform": "server:mcp:athena-platform", "operator": "server:mcp:athena-operator-local", + "web": "server:mcp:web-general-local", } LABELS = { @@ -42,6 +43,7 @@ class Filter: "navidrome": "Navidrome", "platform": "Athena-Plattformwissen", "operator": "Athena Operator", + "web": "allgemeine Websuche (TinySearch)", } def __init__(self): @@ -83,6 +85,7 @@ class Filter: r"\b(?:web|internet|online|websuche|webrecherche)\b", r"\b(?:such|recherchier|pr[uü]f|schau)\w*\b.{0,80}\b(?:netz|web|internet|online)\b", r"\b(?:deezer|youtube|makerworld|wikipedia|docker\s*hub)\b", + r"\b(?:wetter|regen|regnen|vorhersage)\b", r"\b(?:aktuell|neueste|heute|bis heute)\w*\b.{0,100}\b(?:quelle|liste|verf[uü]gbar|erschienen|video|folge)\w*\b", ), ) @@ -123,6 +126,20 @@ class Filter: "contents. Use the read-only shell only when that purpose-built tool " "cannot answer." ) + if "homeassistant" in selected: + rule += ( + " For Home Assistant, start with ha_search, ha_get_state, or an " + "exact entity/configuration query. Never call ha_list_states merely " + "to verify a few referenced entities or automations; use the full " + "state inventory only when the user explicitly requests a complete " + "inventory. For YAML work, inspect the exact file or section first " + "and keep any proposed edit separate from live state verification. " + "Never infer an automation entity_id from its YAML id or alias. After " + "a reload, resolve the actual entity_id through targeted search or the " + "entity registry before calling an automation service. Avoid repeating " + "the same YAML read, state lookup, or validation unless new evidence " + "or a changed configuration makes the repetition necessary." + ) messages = body.setdefault("messages", []) for message in messages: if message.get("role") == "system" and isinstance(message.get("content"), str): @@ -161,7 +178,8 @@ class Filter: homeassistant = self._matches( text, ( - r"\bhome\s*assist(?:ant|ent)s?\b", r"\bhomeassistants?\b", r"\bhass\b", + r"\bhome[-\s]*assist(?:ant|ent)s?\b", r"\bhomeassistants?\b", r"\bhass\b", + r"\bha_[a-z0-9_]+\b", r"\bautomatisierung(?:en)?\b", r"\bentit[aä]t(?:en)?\b", r"\bautomations?\.ya?ml\b", r"\bconfiguration\.ya?ml\b", r"\b(?:sensor|light|switch|climate|automation)\.[\w.-]+", @@ -219,6 +237,7 @@ class Filter: r"\b(?:keine|keinerlei|nichts?)\b.{0,40}\b(?:[aä]nder|update|aktualisier|durchf[uü]hr|installier|download|umbenenn)\w*\b", r"\bohne\b.{0,50}\b(?:[aä]nder|update|aktualisier|durchf[uü]hr|installier|download|umbenenn)\w*\b", r"\b(?:nicht|nichts?)\b.{0,20}\b(?:durchf[uü]hr|ausf[uü]hr|aktualisier|installier)\w*\b", + r"\b(?:[aä]nder|erstell|installier|l[oö]sch|entfern|download|umbenenn)\w*\b.{0,25}\b(?:nichts?|keine[nrms]?|keinerlei)\b", ), ): unraid_write = False @@ -282,8 +301,14 @@ class Filter: return body latest_text = self._latest_user_text(body) - selected = self._classify(latest_text) needs_native_web = self._needs_native_web(latest_text) + selected = self._classify(latest_text) + # Web work gets two independent, general-purpose engines. Native + # OpenWebUI search has good result rendering but can return empty sets + # for an otherwise valid query. TinySearch adds only four compact + # tools and provides a portable fallback without site-specific MCPs. + if needs_native_web and "web" not in selected: + selected.append("web") # General web search is a basic capability, not a site-specific MCP. # Keep it available on every normal request so an unfamiliar website # (MakerWorld, eBay, a vendor page tomorrow) never requires another @@ -298,8 +323,6 @@ class Filter: metadata_features["web_search"] = True if not selected: - if needs_native_web: - await self._notify(__event_emitter__, ["native Websuche"]) return body existing = body.get("tool_ids") @@ -335,10 +358,12 @@ class Filter: for message in body.get("messages", []): if message.get("role") == "system" and isinstance(message.get("content"), str): message["content"] += ( - " Native Open WebUI web search is also enabled for this request. " - "After collecting local or specialist-tool evidence, continue with " - "search_web/fetch_url for current public evidence; do not stop merely " - "because no separate web MCP appears in the MCP tool list." + " Two broad web engines are available: native Open WebUI " + "search_web/fetch_url and TinySearch search/scrape_urls/research. " + "Start with one focused query. If it returns no results, an access " + "block, or weak evidence, switch once to the other engine with a " + "reformulated query. Do not repeat many near-identical searches and " + "do not request or wait for a site-specific MCP." ) break await self._notify(__event_emitter__, labels) diff --git a/platform/openwebui/patch_tool_finalization.py b/platform/openwebui/patch_tool_finalization.py index ed33a39..04275e1 100644 --- a/platform/openwebui/patch_tool_finalization.py +++ b/platform/openwebui/patch_tool_finalization.py @@ -33,6 +33,12 @@ replacement = """ tool_call_iterations = 0 tool_call_executions = 0 max_tool_call_executions = 40 max_executions_per_tool = 12 + # Inlet filters only see the first request, not later internal + # tool continuations. Bound the evidence injected into those + # continuations so one broad inventory cannot consume context. + max_single_tool_result_chars = 12000 + max_total_tool_result_chars = 64000 + tool_result_chars_used = 0 tool_execution_counts = {} tool_signature_counts = {} max_tool_call_iterations = getattr( @@ -96,6 +102,64 @@ if source.count(needle) != 1: ) source = source.replace(needle, replacement) +needle = """ results.append( + { + 'tool_call_id': tool_call_id, + 'content': str(tool_result) if tool_result else '', + **({'files': tool_result_files} if tool_result_files else {}), + **({'embeds': tool_result_embeds} if tool_result_embeds else {}), + } + ) +""" +replacement = """ # Citations and the visible terminal event above use the + # complete result. Only the model continuation is compacted. + tool_result_text = str(tool_result) if tool_result else '' + remaining_tool_chars = max( + 0, max_total_tool_result_chars - tool_result_chars_used + ) + allowed_tool_chars = min( + max_single_tool_result_chars, remaining_tool_chars + ) + if len(tool_result_text) > allowed_tool_chars: + original_tool_chars = len(tool_result_text) + if allowed_tool_chars >= 800: + marker = ( + '\\n\\n[Tool result compacted by Open WebUI: ' + f'{original_tool_chars} characters total; middle omitted. ' + 'Refine the next tool call instead of requesting the same ' + 'broad inventory again.]\\n\\n' + ) + tail_chars = min(2000, allowed_tool_chars // 4) + head_chars = max( + 0, allowed_tool_chars - len(marker) - tail_chars + ) + tool_result_text = ( + tool_result_text[:head_chars] + + marker + + tool_result_text[-tail_chars:] + ) + else: + tool_result_text = ( + '[Tool-result context budget exhausted. Use a more targeted ' + 'tool call or answer from existing evidence.]' + )[:allowed_tool_chars] + tool_result_chars_used += len(tool_result_text) + + results.append( + { + 'tool_call_id': tool_call_id, + 'content': tool_result_text, + **({'files': tool_result_files} if tool_result_files else {}), + **({'embeds': tool_result_embeds} if tool_result_embeds else {}), + } + ) +""" +if source.count(needle) != 1: + raise SystemExit( + f"expected exactly one Open WebUI tool-result anchor, found {source.count(needle)}" + ) +source = source.replace(needle, replacement) + needle = """ res = await generate_chat_completion( request, new_form_data, diff --git a/platform/operator/install-operator.sh b/platform/operator/install-operator.sh index 30ba127..501fdc0 100755 --- a/platform/operator/install-operator.sh +++ b/platform/operator/install-operator.sh @@ -19,6 +19,10 @@ install -d -m 0700 /data/mike-ai-operator/state /data/mike-ai-operator/repositor install -m 0755 "$ROOT/platform/operator/athena_operatord.py" /usr/local/libexec/mike-ai-athena-operatord install -m 0644 "$ROOT/platform/operator/athena-operator.service" /etc/systemd/system/mike-ai-athena-operator.service systemctl daemon-reload -systemctl enable --now mike-ai-athena-operator.service +systemctl enable mike-ai-athena-operator.service +# The unit may already be active during an in-place upgrade. `enable --now` +# does not reload a running process after its executable was replaced, which +# would leave the MCP facade and executor on different protocol versions. +systemctl restart mike-ai-athena-operator.service systemctl is-active --quiet mike-ai-athena-operator.service echo ATHENA_OPERATOR_EXECUTOR_OK