fix(bot-mode): Routines pane scopes to the clicked bot, not the stale gateway profile - #89637
Closed
chelsealong wants to merge 2 commits into
Closed
fix(bot-mode): Routines pane scopes to the clicked bot, not the stale gateway profile#89637chelsealong wants to merge 2 commits into
chelsealong wants to merge 2 commits into
Conversation
… gateway profile Clicking a bot in the roster sets $selectedBot immediately, but the chat connection takes a moment to actually swap gateways, so host.state.profile still reports the previous bot during that gap. RoutinesPane picked the stale gateway profile first, so the pane kept showing the old bot's cron jobs until the connection caught up (NousResearch#89625). $selectedBot already tracks the live gateway profile via a listener plus roster clicks, so it is current the instant a bot is clicked — flip the precedence so it wins.
…eaves it stale nanostores' .listen() never replays the current value the way .subscribe() does, so the profile-sync listener added in the previous commit only kept $selectedBot current from the moment it was attached. A disable -> profile switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at whichever bot was active before the plugin was disabled, and since the previous commit made $selectedBot win over the live gateway profile unconditionally, RoutinesPane would scope to that stale bot indefinitely. Extract the sync into bindProfileSync(), which reseeds $selectedBot from the profile store's current value before attaching the listener. This runs on every register() call, so re-enabling the plugin always starts in sync. Added a regression test that reproduces the stale-value semantics with a fake store mirroring real nanostores listen/get behavior, and confirmed it fails without the reseed (falls back to 'default'/stale value) and passes with it.
teknium1
pushed a commit
that referenced
this pull request
Aug 19, 2026
…eaves it stale nanostores' .listen() never replays the current value the way .subscribe() does, so the $focusedBotProfile listener in register() only kept $selectedBot current from the moment it was attached. A disable -> profile switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at whichever bot was active before the plugin was disabled, so the roster highlight fallback and Routines scoping could start from a stale bot. Extract the sync into bindProfileSync(), which reseeds $selectedBot from the profile store's current value before attaching the listener. This runs on every register() call, so re-enabling the plugin always starts in sync. Salvaged from PR #89637 (the pane-precedence portion was superseded on main by the $focusedBotProfile design; this residual reseed gap remained). Regression test mimics real nanostores get/listen semantics and fails without the reseed. Fixes-residual-of: #89625
Contributor
|
Merged via PR #89717 (#89717) with your authorship preserved. Note: the precedence-flip half was superseded on main by 23c1c98 shortly before you opened this, but your second finding — the missing get()-before-listen reseed of the selected bot — was real and unfixed; that half is what landed, with your regression tests adapted to the new $focusedBotProfile design. Thanks for the sharp diagnosis! |
lisajlau
pushed a commit
to lisajlau/hermes-agent
that referenced
this pull request
Aug 20, 2026
…eaves it stale nanostores' .listen() never replays the current value the way .subscribe() does, so the $focusedBotProfile listener in register() only kept $selectedBot current from the moment it was attached. A disable -> profile switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at whichever bot was active before the plugin was disabled, so the roster highlight fallback and Routines scoping could start from a stale bot. Extract the sync into bindProfileSync(), which reseeds $selectedBot from the profile store's current value before attaching the listener. This runs on every register() call, so re-enabling the plugin always starts in sync. Salvaged from PR NousResearch#89637 (the pane-precedence portion was superseded on main by the $focusedBotProfile design; this residual reseed gap remained). Regression test mimics real nanostores get/listen semantics and fails without the reseed. Fixes-residual-of: NousResearch#89625
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #89625
Bug
In Bot Mode, clicking a different bot in the roster doesn't change which
bot's cron jobs the Routines pane shows — it keeps showing the previously
active bot's jobs (e.g. the default profile's 14 jobs instead of Blog
Writer's 1) until a chat message is sent, and sometimes not even then.
Root cause
RoutinesPane(apps/desktop/src/plugins/hermes-bots/plugin.js) pickedgatewayProfile(host.state.profile, the profile the chat connection iscurrently routed through) ahead of
$selectedBot(the bot the user justclicked):
Clicking a bot in the roster sets
$selectedBotsynchronously, but thegateway connection takes a moment to actually swap over —
host.state.profilestill reports the previous bot during that window. Since the old code
preferred
gatewayProfile, the pane queriedcron.managewith the wrongprofilescope until the connection caught up (and could stay wrong if therouting never fully switched, per the report).
Fix
Flip the precedence and extract the decision into a small pure helper:
RoutinesPanenow callsresolveRoutinesBot(selected, gatewayProfile).Making
$selectedBotwin unconditionally is only safe if$selectedBotisactually always current — and a first version of this fix (reviewed here)
wasn't: the
host.state.profile.listen(...)subscriber that keeps$selectedBotsynced to the live gateway profile is attached inregister()and torn down in
onDispose(). nanostores'.listen()never replays thecurrent value the way
.subscribe()does, so if a user disables Bot Mode,switches the active gateway profile while it's disabled, then re-enables it,
$selectedBotstayed pointed at the pre-disable bot until the next profilechange happened to fire the listener — which, combined with the precedence
flip, would have made RoutinesPane show the wrong bot's jobs indefinitely
(the same failure class as the original bug, just moved to a longer-lived
trigger).
Fixed by extracting the sync into
bindProfileSync(), which reseeds$selectedBotfrom the profile store's current value before attaching thelistener:
register()now callsbindProfileSync(host.state.profile), so everyregister — including a disable → re-enable cycle — starts back in sync with
the live profile before anything can rely on
$selectedBot.Test
apps/desktop/src/plugins/hermes-bots/tests/routines-selected-bot.test.mjs(following this suite's existing
vm-harness pattern, seeroutines-error.test.mjs) covers both the pureresolveRoutinesBothelperand, separately,
bindProfileSync's reseed behavior — using a fake profilestore that mirrors real nanostores
get/listensemantics (listendoesnot replay the current value) rather than a mock that assumes the fix works.
The new regression test drives: bind (reseed to "blog-writer") → unbind
(disable) → profile changes to "researcher" while unbound → bind again
(re-enable) → asserts
$selectedBotandresolveRoutinesBotboth resolve tothe live "researcher" profile, not the stale "blog-writer".
Verified the new regression tests fail without the
bindProfileSyncreseed(reverting just the reseed lines, keeping the plain
.listen()):And that all tests in the file fail against the pre-fix commit, where
bindProfileSyncdoesn't exist at all:All pass with the fix in place:
Full plugin test suite (from
apps/desktop):AI assistance disclosure
This change was authored with AI assistance (Claude).