fix(relay): address native plugin cutover review - #10
Closed
afourniernv wants to merge 224 commits into
Closed
afourniernv wants to merge 224 commits into
afourniernv wants to merge 224 commits into
Conversation
…of band The desktop settings page saves approvals.mode through REST PUT /api/config (and the raw editor through PUT /api/config/raw). Enforcement follows the file immediately, because the approval gate re-reads config per command, but every live session's YOLO/approval indicator repaints only on a session.info event, and the REST save emitted nothing. The indicator showed bypass OFF while approvals.mode=off silently auto-approved every dangerous command, and switching sessions repainted the stale cached per-session state, making the toggle look like it flipped itself back. The gateway /approvals slash command had the same gap. The config.set RPC handler already re-emits session.info to all live sessions after a mode flip; give the other writers the same contract: - tui_gateway/server.py: add broadcast_session_info(), which snapshots _sessions under _sessions_lock and re-emits via _emit_session_info_for_session. Also call it from the /approvals slash mirror when a mode argument was persisted (bare /approvals is read-only). - hermes_cli/web_server.py: after a REST save that actually changed the normalized approvals.mode, call the broadcast through a sys.modules guard (no gateway imported means no sessions to notify). The comparison runs on the in-memory documents (existing vs merged, parsed vs raw): the settings page PUTs the defaulted GET record while disk holds sparse YAML, so a block-level compare would broadcast on every autosave, and re-reading through the config cache after the save could serve the pre-save document on an (mtime_ns, size) key collision. Own-profile saves only: a profile-scoped save targets a different HERMES_HOME than this process's gateway sessions. No broadcast on saves that leave the effective mode unchanged, so settings autosave churn (skin, font, TTS) can't spam session.info. Scope: reaches sessions of the in-process gateway (hermes serve / hermes dashboard, the topologies the desktop app talks to). A spawned tui_gateway.entry child gateway has its own process and _sessions; its TUI statusbar reconciles each turn via the existing session.info emissions.
Remote mode only offered Copy Path, which is a Linux server path and useless on the local machine. Reuse the existing gateway save bridge so a selected file can land on this computer.
Lock the remote-file-only menu gate and the save-bridge success, cancel, and error paths.
…teway
A profile belongs to one gateway, but the Capabilities surface (Skills /
Tools / MCP) always read and wrote through the window's active backend —
scoping to a remote-owned profile silently edited the wrong machine.
- hermes.ts: capability REST helpers accept a ProfileScope
(string | {connectionId, profile}); ambient path now also carries the
active registry connection tag (same contract as the cron helpers,
NousResearch#87882); profileScopeKey namespaces cache keys per connection.
- SkillsView: scope selector lists (profile, device) rows from the union
agent roster on multi-connection desktops; new fixedConnection prop
pins the whole view to a registered connection (plugin door), with a
probe-able SkillsView.supportsFixedConnection flag.
- MCP tab: live reload.mcp RPC withheld for cross-backend scopes (it
rides the active gateway socket and would reload the wrong machine).
- Bot Mode: remote-target drafts now get the live Capabilities tab
pinned to the target machine via fixedConnection, feature-detected so
older desktops keep the staged checklists.
- Config-record/hub-action stores accept scopes; cache keys fold in the
connection id so two gateways' same-named profiles never share rows.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…and after Bot Mode group chats were named once at creation and could never be renamed, and rooms had no picture — only the fanned member faces. - New Group Chat dialog: optional room picture (upload from device or image.generate, same 256px normalize pipeline as bot avatars). - Room header: gear button opens Group settings — rename the group or set/replace/remove the picture after creation. - renameGroupChat re-keys the room record (log, watermarks, sessions, members, picture), swaps the name in every local member's ui_meta groups list, follows open views to the new name, and rejects collisions instead of silently suffixing. Stored member sessions keep resuming by sid, so no history is lost. - Room picture persists in the durable room record, hydrates on window load, and renders in the roster row (over the face pile), and the room header.
Bold (SGR 1) and dim (SGR 2) are independent terminal attributes that share a single reset code (SGR 22). ansi-tokenize's diffAnsiCodes models 'same endCode' as 'same slot' — emitting [2m over a bold cell yields bold+dim instead of dim, and dropping a weight entirely emits nothing. Every such transition leaves the real terminal diverged from the StylePool's tracked state, and since later transitions are computed from that phantom state the corruption compounds and sticks: random spans of wrong weight/brightness that depend on which cells changed in which order — the long-standing 'random dimness/opacity changes at whim' in the TUI. transitionAnsiCodes() wraps the diff: when a weight flag is removed, reset the family with SGR 22 and re-apply the target's weights; pure additions and non-weight styles keep the minimal library diff. Wired into StylePool.transition (cached per-pair, hot diff path) and the full-frame renderer. Proven by an end-to-end probe (LogUpdate frames -> strict SGR interpreter -> compare cell attrs vs the screen model): 18 divergent cells on main, 0 with the fix.
…les (NousResearch#89206 class) The salvaged gate only requested an explicit session.resume when the target stored session was ALREADY selected — but the field failure (NousResearch#89206) is the cold open, where the persisted route points at the bot's session while selection/runtime/transcript are all unsettled. The precondition skipped the resume exactly when it was needed, and the hydration wait timed out into a blank pane. - sdk/index.ts: judge the main surface AFTER openSession() navigates, and request a sequenced resume whenever the surface is not healthy (selected + runtime bound + expected transcript present). Redundant requests are consumed as no-ops by the route-resume effect. - hermes-bots plugin: widen the fix to the sibling open path — the profile session browser (openProfileSession) now opens with the same awaitHydration/expectHistory contract as canonical Bot Chats, so a stale main surface gets the same explicit resume instead of a silent blank pane. - Regression test for the cold-open shape, proven failing against the pre-fix gate (sabotage run) and passing with it.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Group-chat message bodies in the hermes-bots plugin render without the
data-selectable-text attribute, so they inherit the app-wide
body { user-select: none } and cannot be drag-selected or copied.
1:1 chat messages already carry the equivalent marker
(aui_assistant-message-content), so this aligns group chat with that
behavior by adding data-selectable-text="true" to the message body
wrapper.
Follow-up to the root-cause fix: comment documenting why the opt-in exists, plus a source-contract test proven to fail without the attribute (sabotage run).
…eting id getAllTranscripts webhooks put the callTranscript id in resourceData.id. Using that as an onlineMeeting id makes Graph v1.0 return 400 Unexpected id format. Parse meeting and organizer ids from @odata.id and use the organizer-scoped users path.
Pin that transcript notifications keep the onlineMeeting id from @odata.id, and that meeting GET uses the organizer-scoped Graph path.
The getAllTranscripts resourceData.id from the field report is a base64url blob whose DECODED payload ends in "-TranscriptV2" while the encoded form contains no readable marker, so the substring heuristic in looks_like_transcript_id missed it. Add a best-effort base64 decode hint so degraded notifications (no @odata.id) are still refused with the clear guidance error instead of a cryptic Graph 400.
Maps vadelma@agenttiklubi.org and the bare noreply address to vadelma-agent, and taneli.mielikainen@iki.fi to tmielika, so the check-attribution gate passes on their open PRs (NousResearch#70667, NousResearch#72671, NousResearch#67934, NousResearch#86255, NousResearch#89194).
Port the stale PR NousResearch#61665 behavior to current main. The original two-file contribution is by yungchentang; this candidate preserves its scoped Codex OAuth fallback intent. Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
require.resolve returns the macOS realpath (/private/var/...) while os.tmpdir() stays on the /var symlink, so a raw join() deepEqual failed even though the spawn was correct.
…ktop-rebuild-status fix(update): don't report success when the Desktop rebuild failed (supersedes NousResearch#88359, NousResearch#87984)
…covery-command fix(desktop): recover missing get-windows binding
…oup-icon-fix fix(desktop): close the blank hole in the Sessions header
…-regain-atomic-repaint fix(tui): heal focus regain without a separate screen clear (supersedes NousResearch#88596)
Supersedes NousResearch#75649, NousResearch#86637, NousResearch#87672, NousResearch#88642. Fixes NousResearch#86601 Fixes NousResearch#87652 Fixes NousResearch#87823 Co-authored-by: Charmmy <lilShawtty@qq.com> Co-authored-by: chelsealong <chelsealong@126.com> Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com> Co-authored-by: Ricardo Mendes <ricardo.mendes@maiolabs.ai>
Three local-environment leaks made tests red locally while green on CI: - tests/conftest.py: blank HERMES_REAL_HOME and TERMINAL_HOME_MODE per test. The terminal tool injects both into subprocess envs, so any pytest run launched from a Hermes session inherits them and the hermes_constants home-resolution helpers prefer HERMES_REAL_HOME over the monkeypatched HOME (4 failures in test_subprocess_home_isolation.py). - test_modal_sandbox_fixes.py: reset the import-time _YOLO_MODE_FROZEN flag and pin approval mode to manual in _isolate_approval_state(). HERMES_YOLO_MODE=1 in the launching shell froze True at collection time and every guard auto-approved (2 failures). - test_noninteractive_git.py: strip GIT_ASKPASS/VS Code askpass vars in the fail-fast clone E2E. noninteractive_git_env() intentionally keeps a working askpass helper, but this test asserts the no-helper path; under VS Code the helper blocks on the editor until the 30s timeout (1 failure). Verified: all 49 tests in the three files pass both in a plain dev shell (with HERMES_YOLO_MODE=1, HERMES_REAL_HOME, and VS Code askpass set) and inside an unshare -rn network namespace.
… video pickers Same bug class as the plugin image picker crash (NousResearch#77238): an unguarded `current_model = default_model` fallback that can index the catalog with a key it doesn't contain when the provider's default drifts from its catalog. Applies the `default if default in catalog else next(iter(catalog))` guard to _configure_imagegen_model and _configure_videogen_model_for_plugin.
…e-image-2.0 selectable - plugins/image_gen/xai: merge the live /v1/image-generation-models catalog (5-min cache, 10s timeout, static-table fallback when offline/unauth) into the picker so new xAI Imagine models appear automatically the day they launch, with generic metadata until curated text is added. - Add grok-imagine-image-2.0 to the curated static table (typography/ layout-aware model, API-available since Aug 8 2026). - Edits honor an explicitly selected image-input-capable model (e.g. grok-imagine-image-2.0) instead of always forcing grok-imagine-image-quality; quality remains the default edit baseline. - Tests: hermetic autouse fixture keeps unit runs offline; new coverage for live-merge, unknown-future-model selection, offline fallback, and edit-model resolution. Docs model table updated (en + zh-Hans). Live-verified: /image-generation-models returns grok-imagine-image, grok-imagine-image-2.0, grok-imagine-image-quality; real generation with 2.0 succeeded end to end.
This reverts commit 0f73adb.
Adds xai/grok-imagine-image/v2.0/text-to-image with edit_endpoint xai/grok-imagine-image/v2.0/edit (max 3 reference images). 1k/2k resolution, low/medium quality (pinned 1k+medium = $0.06/image), 13 aspect ratios (we map the standard 3), no seed param in the schema. upscale=True (1k native sub-2MP). Schema verified against fal.ai llms.txt + OpenAPI.
…stores 1.4.2) nanostores 1.4.0-1.4.1 annotate batch() @__NO_SIDE_EFFECTS__. Rollup (via vite build) honors that and erases a result-unused batch(...) call as dead code -- callback included. Since d57f94a/053eb7aab/4e520f085 moved the gateway-switch publication (activate() + + ) inside batch(), packaged desktop builds lost the entire publication: clicking a profile in the rail did nothing at all. Dev builds and vitest run unminified, so only the packaged app broke. nanostores 1.4.2 removes the annotation from batch() (it stays on the creation functions, where it is correct). Bump all three pinned copies (apps/desktop, apps/bootstrap-installer, ui-tui) and add a regression test asserting the installed nanostores never re-annotates batch.
…Research#88404) Adapter ingress derives a session key BEFORE the runner stamps source.profile in _make_profile_message_handler, so the namespace fell back to the active profile and every bot in a multiplexed gateway produced agent:main:<platform>:<chat>. A Telegram private chat reports the user's own id as chat.id, identical for every bot, so two profiles sharing one human collapsed onto a single lane: _pending_text_batches, _active_sessions, the busy-session guard and _post_delivery_callbacks are all keyed on that string. A day of production logs across two bots shows 60 flushes, none carrying the secondary profile's namespace. set_owner_profile records credential ownership on the adapter and _session_key_profile resolves the namespace as source.profile -> _owner_profile -> the session store's resolver, so a secondary adapter keys into its own namespace even before the source is stamped. Stamped sources keep priority, so relay/connector ingress, which routes per event rather than per credential, is unchanged. _configure_profile_adapter installs the owner alongside the other handlers, covering startup and reconnect. Every candidate is type-checked as a non-blank str, and every attribute read goes through getattr: adapters are routinely built without BasePlatformAdapter.__init__, and a duck-typed session store returns a truthy non-string that would otherwise be interpolated into the key as agent:<MagicMock ...>:. Also routes the four call sites that passed no profile at all (feishu media batches, raft, slack _session_key_for_source, telegram photo batches) through the same resolver. test_multiplex_busy_input_mode's secondary-adapter busy case seeded _active_sessions with the unstamped agent:main: key, asserting the pre-fix collapse. It now seeds the lane the profile-owned adapter actually derives. A primary adapter has no owner and an unstamped source, so it resolves exactly as before; with multiplex_profiles off the resolver returns None and every key is byte-identical to today's.
…t model; xAI edits honor dispatched model - plugins/image_gen/openrouter: list_models() now queries the endpoint's /models catalog filtered to output_modalities containing "image" (per-backend 5-min cache, 10s timeout, static 2-model chain as offline fallback; openrouter/auto* router pseudo-models excluded). Every image model OpenRouter serves — including future releases — is selectable in `hermes tools` with no code change. Applies to Nous Portal too via the shared provider class. - plugins/image_gen/xai: forward the dispatched model kwarg into _resolve_edit_model() so an explicitly selected edit-capable model is honored on /images/edits (extends the salvaged NousResearch#55893 fix to the edit path; text-only models still fall back to quality). - Tests: OpenRouter live-catalog filtering/exclusions/order, offline fallback, cache single-fetch; xAI edit-kwarg forwarding incl. the text-only-hijack negative case. Live-verified against openrouter.ai: 9 image-output models returned and rendered, matching the public models?output_modalities=image listing.
… (0-job startup_failure on every dispatch; identical content dispatches fine under a new path, proven by probe PR NousResearch#89894)
This reverts commit ab173e2.
…4, and failed installs surface
Three fixes for the "Install on this agent" pipeline, covering the whole
split-brain class between action-spawning endpoints and their status polls:
1. electron/connection-config.ts — the /api/actions/{name}/status poll family
now routes to the same backend as every action-spawning route. Before,
POST /api/skills/hub/install ran on the PRIMARY backend (scoped route)
while the follow-up status poll for a non-default profile routed to the
profile's POOLED backend, which never registered the dynamic action name
(skills-install-<slug>-<hash> lives only in the spawning process's
memory) -> 404 "Unknown action" toast even though the install succeeded.
POST /api/mcp/catalog/install joins the scoped table for the same reason.
2. src/store/hub-actions.ts — a non-zero subprocess exit now rejects with the
action log tail so the caller's catch toasts it. Before, a failed install
(scan gate, network, bad identifier) stopped silently: no toast, no row
flip, and the unchanged skills list read as "install did nothing".
3. src/contrib/runtime-loader.ts — a disk plugin copy shadowed by a bundled
twin now publishes a visible "(stale disk copy)" inventory row carrying
the folder path, instead of a console.info nobody sees. Stale
desktop-plugins/ leftovers from dev deploys are the same folders that
actively break the feature on shells without the bundled twin.
…ween running bots
resolveStoredSession never probed the ACTIVE profile: the unscoped
/api/sessions GET routes to the PRIMARY backend (not the active
gateway's), and the cross-profile probe loop explicitly skipped the
active key. A hidden Bot Mode canonical chat — never present in the
sidebar cache — owned by the focused bot therefore resolved to
undefined on every switch. The transcript prefetch then went unscoped
to the primary backend, 404'd, and the thread painted empty until the
user opened the session explicitly via right-click → Sessions (which
seeds the cache with a profile-stamped row).
Probe the active profile first in the by-id ladder, so hidden and
uncached sessions on the focused profile resolve with ownership and the
prefetch routes to the owning backend.
Live-repro'd headless via CDP with 3 bot profiles mid-turn: before the
fix every focus-switch painted a blank thread ('Waking up <bot>…');
after, the full transcript paints. Reported by @tbkbossswaglord.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Author
|
Superseded by #11, which contains only the review fix on Bryan's current head. The main catch-up has been left to Bryan so the patch remains reviewable. |
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.
Important
Merge this follow-up with a merge commit. Do not squash or rebase it; the branch contains an intentional
mainmerge that preserves all 17 of Bryan's original commits unchanged.Summary
Addresses the valid blocker from the latest review of NousResearch#77915 and catches Bryan's branch up to current
mainwithout rewriting history.maininto Bryan's existing head; no rebase or force-pushDISABLED) from selected-but-invalid configuration (FAILED)FAILEDresult is inherited by later profiles and may retry only after the final owner exitsDeliberately not included
Validation
196 passedacross focused Relay runtime, LLM, tool, plugin cutover, plugin-manager, and shared-metrics testsruff check .python3 scripts/check-windows-footguns.py --alluv lock --checkgit diff --checkSigned-off commits preserve DCO. Fresh exact-head CI is still required after this is merged into NousResearch#77915.