fix(ide): upgrade managed agent and migrate existing member media connection
This commit is contained in:
@@ -2,13 +2,22 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import { test } from 'vitest'
|
import { test } from 'vitest'
|
||||||
|
|
||||||
import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state'
|
import { classifyActiveRuntime, hasValidBootstrapMarker, needsPackagedRuntimeUpgrade } from './active-runtime-state'
|
||||||
|
|
||||||
const VALID_MARKER = {
|
const VALID_MARKER = {
|
||||||
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
|
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
|
||||||
schemaVersion: 1
|
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', () => {
|
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
|
||||||
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
|
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,18 @@ export interface BootstrapMarkerLike {
|
|||||||
schemaVersion?: unknown
|
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 {
|
export interface ActiveRuntimeState {
|
||||||
hasValidMarker: boolean
|
hasValidMarker: boolean
|
||||||
shouldUseActiveRuntime: boolean
|
shouldUseActiveRuntime: boolean
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
systemPreferences
|
systemPreferences
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
|
|
||||||
import { classifyActiveRuntime } from './active-runtime-state'
|
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
|
||||||
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
|
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
|
||||||
import { appIconCandidates, resolveAppIcon } from './app-icon'
|
import { appIconCandidates, resolveAppIcon } from './app-icon'
|
||||||
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
|
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
|
||||||
@@ -4905,6 +4905,11 @@ function resolveHermesBackend(backendArgs) {
|
|||||||
// bootstrap when the runtime itself is unusable.
|
// bootstrap when the runtime itself is unusable.
|
||||||
const activeRuntime = activeRuntimeState()
|
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.shouldUseActiveRuntime && !bootstrapRepairRequested) {
|
||||||
if (!activeRuntime.hasValidMarker) {
|
if (!activeRuntime.hasValidMarker) {
|
||||||
rememberLog(
|
rememberLog(
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
"""Connect the AITURK distribution's media tools to its existing member key."""
|
"""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:
|
def configure_media(cfg: dict, entry: dict) -> bool:
|
||||||
if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1":
|
if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1":
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ def _eager_reconcile_own_session_db() -> None:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _lifespan(app: "FastAPI"):
|
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_channels = {} # dict[str, set]
|
||||||
app.state.event_lock = asyncio.Lock()
|
app.state.event_lock = asyncio.Lock()
|
||||||
app.state.pty_active_session_files = {} # dict[str, Path]
|
app.state.pty_active_session_files = {} # dict[str, Path]
|
||||||
|
|||||||
@@ -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():
|
def test_member_key_is_referenced_without_copying_or_overwriting_other_servers():
|
||||||
|
|||||||
Reference in New Issue
Block a user