feat(gateway): multiplexer hot-serves profiles created while it runs, unroutes deleted ones

A `gateway.multiplex_profiles` gateway enumerated `profiles/` once at boot, so a profile
created afterwards (CLI, dashboard, Desktop, TUI) was never served until `hermes gateway
restart`; Desktop and the dashboard gave no reminder, so a new profile's bot simply never
connected.

The served set is now reconciled at runtime (`gateway/run_profile_reconcile.py`):
- `hermes_cli/profiles.py` create/delete ping the multiplexer over its control socket
  (new `rescan-profiles` verb); a supervised watcher rescans every 30s as the safety net.
- A new profile gets its adapters under its own runtime scope from its config/.env
  (`_start_one_profile_adapters`, same duplicate-credential guard as boot, now seeded
  with the LIVE secondaries' claims), `served_profiles` in gateway_state.json is
  updated, MCP discovery + log routing run for it. Other profiles' adapters are never
  touched.
- A served profile whose config.yaml/.env changed is re-scanned so a token added after
  create builds the adapter; already-live/queued platforms are skipped (no second poller).
- A deleted profile (tombstone) has its reconnects cancelled, adapters torn down,
  pairing/busy bookkeeping and cached agents dropped, and this process's SQLite /
  memory-store handles released so the deleter's rmtree succeeds.
- The in-process cron ticker takes a live enumerator so new profiles' jobs fire.
- PUT /api/messaging/platforms/<id>?profile=X returns `hot_served` when a live
  multiplexer rebuilt X's adapters; Desktop/dashboard skip the restart banner then.
- `hermes profile create` confirms hot-serve; the restart reminder stays for a gateway
  that did not pick the profile up (older build / signal failed).
This commit is contained in:
teknium1
2026-09-12 07:55:28 -07:00
committed by Teknium
parent 0d6b59fb74
commit d1dbb0ac9e
25 changed files with 585 additions and 37 deletions

View File

@@ -875,8 +875,10 @@ export const api = {
// Messaging platforms (gateway channels)
getMessagingPlatforms: () =>
fetchJSON<MessagingPlatformsResponse>("/api/messaging/platforms"),
// `hot_served`: a live multiplexer serving the selected named profile rebuilt its adapters from the
// new credentials right away (no gateway restart needed).
updateMessagingPlatform: (id: string, body: MessagingPlatformUpdate) =>
fetchJSON<{ ok: boolean; platform: string }>(
fetchJSON<{ ok: boolean; platform: string; hot_served?: boolean }>(
`/api/messaging/platforms/${encodeURIComponent(id)}`,
{
method: "PUT",

View File

@@ -215,11 +215,17 @@ export default function ChannelsPage() {
setSaving(true);
try {
const body: MessagingPlatformUpdate = { env, enabled: true };
await api.updateMessagingPlatform(editing.id, body);
showToast(`${editing.name} saved`, "success");
const result = await api.updateMessagingPlatform(editing.id, body);
showToast(
result.hot_served
? `${editing.name} saved; the running gateway is connecting`
: `${editing.name} saved`,
"success",
);
setEditing(null);
setRestartNeeded(true);
if (!result.hot_served) setRestartNeeded(true);
await load();
if (result.hot_served) setTimeout(() => void load(), 4000);
} catch (e) {
showToast(`Failed to save: ${e}`, "error");
} finally {
@@ -231,7 +237,7 @@ export default function ChannelsPage() {
const next = !platform.enabled;
setTogglingId(platform.id);
try {
await api.updateMessagingPlatform(platform.id, { enabled: next });
const result = await api.updateMessagingPlatform(platform.id, { enabled: next });
setPlatforms((prev) =>
prev.map((p) =>
p.id === platform.id
@@ -239,7 +245,8 @@ export default function ChannelsPage() {
: p,
),
);
setRestartNeeded(true);
if (result.hot_served) setTimeout(() => void load(), 4000);
else setRestartNeeded(true);
} catch (e) {
showToast(`Error: ${e}`, "error");
} finally {