From 8f38fd53e439bfb0e3937afb0bf7c09d6acab332 Mon Sep 17 00:00:00 2001 From: yilsem Date: Sat, 5 Sep 2026 15:35:04 +0300 Subject: [PATCH] fix(ide): upgrade managed agent and migrate existing member media connection --- apps/desktop/electron/active-runtime-state.test.ts | 11 ++++++++++- apps/desktop/electron/active-runtime-state.ts | 12 ++++++++++++ apps/desktop/electron/main.ts | 7 ++++++- hermes_cli/aiturk_media.py | 10 ++++++++++ hermes_cli/web_server.py | 6 ++++++ tests/unit/test_aiturk_media.py | 11 ++++++++++- 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/apps/desktop/electron/active-runtime-state.test.ts b/apps/desktop/electron/active-runtime-state.test.ts index afc3e74..2c99b15 100644 --- a/apps/desktop/electron/active-runtime-state.test.ts +++ b/apps/desktop/electron/active-runtime-state.test.ts @@ -2,13 +2,22 @@ import assert from 'node:assert/strict' import { test } from 'vitest' -import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state' +import { classifyActiveRuntime, hasValidBootstrapMarker, needsPackagedRuntimeUpgrade } from './active-runtime-state' const VALID_MARKER = { pinnedCommit: '1234567890abcdef1234567890abcdef12345678', schemaVersion: 1 } +test('a package upgrade refreshes only a proven managed runtime, once per package', () => { + const stamp = {commit: 'b'.repeat(40), source: 'git'} + assert.equal(needsPackagedRuntimeUpgrade(true, stamp, VALID_MARKER), true) + assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: stamp.commit}), false) + assert.equal(needsPackagedRuntimeUpgrade(false, stamp, VALID_MARKER), false) + assert.equal(needsPackagedRuntimeUpgrade(true, stamp, null), false) + assert.equal(needsPackagedRuntimeUpgrade(true, {commit: '0'.repeat(40), source: 'fallback'}, VALID_MARKER), false) +}) + test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => { assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true) }) diff --git a/apps/desktop/electron/active-runtime-state.ts b/apps/desktop/electron/active-runtime-state.ts index 6d922a4..12bda00 100644 --- a/apps/desktop/electron/active-runtime-state.ts +++ b/apps/desktop/electron/active-runtime-state.ts @@ -3,6 +3,18 @@ export interface BootstrapMarkerLike { schemaVersion?: unknown } +// An AITURK package upgrade must bring its managed agent along with the UI. +// Developer checkouts and installs without proven Desktop ownership keep their +// existing launch behavior. The installer preserves edits and refuses rollback. +export function needsPackagedRuntimeUpgrade( + packaged: boolean, stamp: { commit?: string; source?: string } | null, + marker: BootstrapMarkerLike | null +): boolean { + return Boolean(packaged && stamp?.source !== 'fallback' && + /^[0-9a-f]{40}$/i.test(stamp?.commit || '') && !/^0+$/.test(stamp?.commit || '') && + hasValidBootstrapMarker(marker, 1) && marker?.pinnedCommit !== stamp?.commit) +} + export interface ActiveRuntimeState { hasValidMarker: boolean shouldUseActiveRuntime: boolean diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 48df4c8..f30a367 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -30,7 +30,7 @@ import { systemPreferences } from 'electron' -import { classifyActiveRuntime } from './active-runtime-state' +import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state' import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport' import { appIconCandidates, resolveAppIcon } from './app-icon' import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child' @@ -4905,6 +4905,11 @@ function resolveHermesBackend(backendArgs) { // bootstrap when the runtime itself is unusable. const activeRuntime = activeRuntimeState() + if (needsPackagedRuntimeUpgrade(IS_PACKAGED, INSTALL_STAMP, readBootstrapMarker())) { + rememberLog('[bootstrap] AITURK package changed; updating its managed agent before launch.') + return createBootstrapBackend(backendArgs) + } + if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) { if (!activeRuntime.hasValidMarker) { rememberLog( diff --git a/hermes_cli/aiturk_media.py b/hermes_cli/aiturk_media.py index c913992..be02567 100644 --- a/hermes_cli/aiturk_media.py +++ b/hermes_cli/aiturk_media.py @@ -1,6 +1,16 @@ """Connect the AITURK distribution's media tools to its existing member key.""" +def configure_existing_media(cfg: dict) -> bool: + """Migrate an existing member endpoint when the upgraded backend starts.""" + providers = cfg.get("providers") or {} + entries = providers.values() if isinstance(providers, dict) else [] + for entry in entries: + if isinstance(entry, dict) and configure_media(cfg, entry): + return True + return False + + def configure_media(cfg: dict, entry: dict) -> bool: if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1": return False diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index d2ecbfc..a8c1853 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -405,6 +405,12 @@ def _eager_reconcile_own_session_db() -> None: @asynccontextmanager async def _lifespan(app: "FastAPI"): + from hermes_cli.aiturk_media import configure_existing_media + from hermes_cli.config import read_raw_config, save_config + with _CONFIG_MUTATION_LOCK: + media_cfg = read_raw_config() + if configure_existing_media(media_cfg): + save_config(media_cfg) app.state.event_channels = {} # dict[str, set] app.state.event_lock = asyncio.Lock() app.state.pty_active_session_files = {} # dict[str, Path] diff --git a/tests/unit/test_aiturk_media.py b/tests/unit/test_aiturk_media.py index 180df1a..79d5f02 100644 --- a/tests/unit/test_aiturk_media.py +++ b/tests/unit/test_aiturk_media.py @@ -1,4 +1,13 @@ -from hermes_cli.aiturk_media import configure_media +from hermes_cli.aiturk_media import configure_media, configure_existing_media + + +def test_existing_member_profile_migrates_once_without_changing_provider(): + entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "MEMBER_KEY"} + cfg = {"providers": {"turkservis": dict(entry)}, "model": {"default": "kept"}} + assert configure_existing_media(cfg) + assert cfg["providers"]["turkservis"] == entry + assert cfg["model"] == {"default": "kept"} + assert not configure_existing_media(cfg) def test_member_key_is_referenced_without_copying_or_overwriting_other_servers():