fix(profiles): ship a retry path for the rename identity migration

A rename under a live multiplexer that could not reach the control verb warned and
stopped there, leaving the operator with no way to finish: the rename cannot be
repeated (profiles/<old> is gone) and the CLI deliberately never rewrites the
routing DB a live gateway holds in memory.

- `hermes profile migrate-identity <old> <new>`: retries the migration —
  delegates to the gateway control verb while a multiplexer is live, performs the
  durable rewrite of both state DBs when none is. Idempotent, and exits non-zero
  naming the offending database on a collision, a lock, or a partial failure. Only
  the name format and the existence of the new profile are checked; the old profile
  directory is expected to be gone.
- An older gateway that does not implement the verb is reported as such (`identify`
  answers while the migrate verb does not), not as "no gateway".
- `_migrate_profile_identity` returns an explicit success/failure result so the
  command can set its exit code; the rename warning now names the exact invocation.
- A failed control answer keeps the raw payload when it carries no reason field.
- The offline failure branch called `click.echo` in a module that never imports
  `click`: a failed second database raised NameError instead of printing its warning.
This commit is contained in:
xielevi
2026-09-15 23:25:56 +08:00
committed by Teknium
parent 4ba717df12
commit 81140e4546
7 changed files with 214 additions and 15 deletions
+18 -2
View File
@@ -9,10 +9,10 @@ from __future__ import annotations
from pathlib import Path
import os
import sys
from typing import Optional
from typing import NoReturn, Optional
def _die(msg: str, code: int = 1, *, err: bool = False) -> None:
def _die(msg: str, code: int = 1, *, err: bool = False) -> NoReturn:
print(msg, file=sys.stderr if err else sys.stdout)
sys.exit(code)
@@ -436,6 +436,21 @@ def _profile_rename(args):
_die(f"Error: {e}")
def _profile_migrate_identity(args):
"""Retry the identity migration of a rename that already completed. Exits non-zero when a
live gateway would not migrate (it still owns the routing index in memory), or when a
database rejected the rewrite (collision, lock, partial failure)."""
from hermes_cli.profiles import migrate_profile_identity
try:
migrated = migrate_profile_identity(args.old_name, args.new_name)
except (ValueError, FileNotFoundError) as e:
_die(f"Error: {e}")
if not migrated:
_die(f"Error: session identity was not migrated. Restart or stop the gateway, then run:\n"
f" hermes profile migrate-identity {args.old_name} {args.new_name}", err=True)
print(f"✓ Session/routing identity migrated: {args.old_name} → {args.new_name}")
def _profile_export(args):
from hermes_cli.profiles import export_profile, get_profile_export_path
name = args.profile_name
@@ -575,6 +590,7 @@ PROFILE_ACTIONS = {
'show': _profile_show,
'alias': _profile_alias,
'rename': _profile_rename,
'migrate-identity': _profile_migrate_identity,
'export': _profile_export,
'import': _profile_import,
'install': _profile_install,