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:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user