merge upstream (88+ commits): resolve the 26-file conflict, restore consolidation-lost code, green the Tests workflow#152
Merged
Merged
Conversation
…earch#43103) The Anthropic picker returned the live /v1/models dump verbatim whenever credentials were configured. Anthropic's API lags newly-routed curated aliases (e.g. claude-fable-5, reachable on Anthropic before the models endpoint enumerates it), so the curated entry vanished from the picker. Merge curated _PROVIDER_MODELS["anthropic"] with the live catalog — curated first, live-only appended, deduped — mirroring the OpenAI curated-merge path. Live failure / no creds falls back to curated verbatim.
Finder/OS drops became `@file:/Users/...` refs that only resolve when the gateway shares the local disk, so on a remote gateway non-image files (PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the file.attach / image.attach_bytes upload pipeline — in-app project-tree and gutter drags stay inline workspace-relative refs — across every drop surface: the conversation area, the composer form, the contenteditable input, and the message-edit composer (which still reproduced the bug). Also: - upload dropped files eagerly when a session exists, so the card shows a spinner instead of stalling the send (images stay submit-time to avoid racing their thumbnail write); - round the attachment card and drop the monospace detail; - render image previews from the bytes we already hold, so a pasted/dropped screenshot shows its thumbnail and previews even when its only on-disk copy is a transient path (the data URL is not persisted to localStorage). Supersedes NousResearch#38615, NousResearch#41203. Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
The in-flight user bubble seeded image attachment refs as `@image:<localpath>`.
In remote-gateway mode that path lives on the desktop, not the gateway, so the
inline thumbnail fetch hit /api/media and 403'd ("Path outside media roots"),
flashing a fallback chip until submit uploaded the bytes.
Seed (and keep) image refs as the raw base64 preview data URL instead. It
renders inline via extractEmbeddedImages with zero network, and survives the
post-sync rewrite (the agent gets the bytes through the attached-image pipeline,
not this display ref) so the thumbnail no longer remounts/flashes. Non-image
refs are unchanged.
Adds optimisticAttachmentRef + unit coverage.
Both jobs in tests.yml (`test` matrix and `e2e`) start from a cold uv cache on every run and install deps with `uv pip install -e ".[all,dev]"`, which re-resolves pyproject.toml ranges and rebuilds the editable install each time. Two changes: 1. Enable uv's official CI caching via setup-uv's `enable-cache: true`, keyed on pyproject.toml + uv.lock, plus `uv cache prune --ci` to keep the persisted cache small. Warm runs install from cache instead of re-downloading/building wheels. 2. Replace the manual `uv venv` + `uv pip install -e` with `uv sync --locked --python 3.11 --extra all --extra dev`. sync installs the exact pinned set from uv.lock (and fails if the lock is stale vs pyproject.toml), creating .venv itself. This is reproducible and, with a warm cache, measurably faster than the editable pip install (~3-4x on the steady-state install step locally). Downstream steps keep using `source .venv/bin/activate`; sync writes .venv to the same path. Follows the Astral-recommended pattern for uv in GitHub Actions: https://docs.astral.sh/uv/guides/integration/github/ Co-authored-by: Wesley Simplicio <wesleysimplicio@live.com>
Two races in the drop-time eager upload: - Resurrected chip: the success path used addComposerAttachment, which re-appends when the id is gone, so a file removed mid-upload reappeared once the upload resolved. Add updateComposerAttachment (update-only; no-op when the chip was removed) and use it on both the eager success path and submit-time sync. - Duplicate upload: submit-time sync didn't join an eager upload still in flight, so drop-then-Enter could fire file.attach twice and leave a duplicate under .hermes/desktop-attachments/. Track in-flight eager uploads by id and await the pending one before deciding to re-upload, reusing its gateway ref. Tests: composer-store no-resurrect unit tests + a join-on-submit integration test asserting a single file.attach. Addresses @helix4u review on NousResearch#43109.
…ops upload The message-edit composer staged dropped OS files asynchronously with no visible state, so confirming the edit before the upload resolved could send the message without the gateway-side ref (helix4u review note on NousResearch#43109). Add a staging flag: while uploadOsDropRefs is in flight, show a small spinner pill in the bubble and block submit (disabled send button + submitEdit guard) so the edit can't outrace the ref insertion. New `attachingFile` i18n string across en/zh/zh-hant/ja.
…6MB cap Remote attachments read their bytes through the readFileDataUrl IPC, which is hard-capped at 16MB and rejects with a raw "file is too large (N bytes; limit M bytes)" string straight into the failure toast (helix4u review note on NousResearch#43109). Translate that into "<file> is too large to upload to the remote gateway (max 16 MB)", parsing the limit out of the message so it tracks the real cap. Applies to both the image and non-image remote read paths; non-cap errors pass through unchanged. Adds unit coverage for both.
…s reappear (NousResearch#43149) * fix(state.db): recover from malformed sqlite_master so hidden sessions reappear The corruption class behind "Desktop/Dashboard show no sessions while hundreds of session files sit on disk" is a malformed sqlite_master — most often a duplicate object row, e.g. two CREATE VIRTUAL TABLE messages_fts entries — surfacing as: sqlite3.DatabaseError: malformed database schema (messages_fts) - table messages_fts already exists SQLite parses the whole schema while preparing the FIRST statement on a connection, so on this class every statement fails before it runs: PRAGMA journal_mode (which is where SessionDB.__init__ actually trips, in apply_wal_with_fallback, BEFORE _init_schema), PRAGMA integrity_check, and even DROP TABLE. The only operations that still work are PRAGMA writable_schema=ON plus direct sqlite_master surgery. A plain FTS-index rebuild at the _init_schema layer therefore cannot reach or fix this; the canonical sessions/messages rows are intact — only the derived schema is broken. Add a dedicated recovery that operates where the failure actually happens: - hermes_state.repair_state_db_schema(): backs up the raw file first, then a least-destructive ladder — (1) de-duplicate sqlite_master keeping the lowest rowid per object (preserves the existing FTS index), escalating to (2) drop every messages_fts* schema object + VACUUM and let the next open rebuild the FTS index from messages. sessions/messages are never modified. Plus is_malformed_db_error() to discriminate this class. - SessionDB.__init__ auto-heals: on a malformed-schema open error it repairs once (process-guarded against loops / concurrent web_server opens) and reopens, so Desktop/Dashboard recover on their own instead of silently showing "no sessions". - hermes doctor --fix detects the malformed class and repairs it (reporting the recovered session count + backup name). - hermes sessions repair [--check-only] [--no-backup] runs on the raw file path, since SessionDB() itself cannot open a malformed DB. Supersedes NousResearch#32589 and NousResearch#33869: both targeted FTS corruption but gated their repair behind statements (integrity_check / SELECT / DROP TABLE) that themselves fail on this class, and neither addressed the apply_wal_with_fallback open-time failure. Credit preserved via Co-authored-by. Closes NousResearch#33865. Co-authored-by: João Vitor Cunha <145560011+plcunha@users.noreply.github.com> Co-authored-by: Tuna Dev <273476039+tuancookiez-hub@users.noreply.github.com> * test(state.db): cover strat-B escalation + unrepairable safe-fail paths --------- Co-authored-by: João Vitor Cunha <145560011+plcunha@users.noreply.github.com> Co-authored-by: Tuna Dev <273476039+tuancookiez-hub@users.noreply.github.com>
…ad end A binary @file: ref (PDF, docx, spreadsheet, …) expanded to a bare "binary files are not supported" warning with no content. The model saw a failure and gave up — e.g. a dropped PDF came back as a text note claiming the type was unsupported, even though the file was staged on disk right next to it. Inject an actionable content block instead: the path, mime type, size, and a nudge to use its tools to read/convert/view the file (and explicitly not to tell the user the type is unsupported). General across every binary type — not PDF-specific. The file already resolves where the agent's tools run (local cwd or the staged copy in a remote session workspace), so it can act on it directly.
…emote-attach-drops fix(desktop): stage dropped files into the remote session workspace
…42871) The "..." overflow that opens the profile manager (the only UI to edit a profile's SOUL.md) was gated behind profiles.length > 1, so a user with only the default profile couldn't edit its persona without first creating a throwaway second profile. Render it unconditionally.
…ile switch (NousResearch#40892) * fix(tui_gateway): honor target profile's terminal.cwd on desktop profile switch The desktop's app-global remote mode serves every profile from one tui_gateway backend, so the process-global TERMINAL_CWD only reflects the launch profile. After switching profiles, a new session resolved its workspace from that stale env var and inherited the previous profile's directory. Add _profile_configured_cwd() to read a non-launch profile's own terminal.cwd from its config.yaml (skipping placeholder/empty/missing and non-existent paths so callers fall back cleanly), and wire it into _completion_cwd() with precedence: explicit client cwd -> existing session cwd -> bound profile's configured cwd -> TERMINAL_CWD -> os.getcwd(). Fixes NousResearch#40334 * test(tui_gateway): cover per-profile cwd resolution (NousResearch#40334) Pin the new contract: _profile_configured_cwd reads a profile's own terminal.cwd and rejects placeholders/missing paths, and _completion_cwd prefers a bound profile's cwd over a stale launch-profile TERMINAL_CWD while still letting an explicit client cwd win.
… state (NousResearch#39639) * fix(desktop): send on Enter from live editor text, not stale composer state Pressing Enter often did nothing (~90% with IME / fast typing); adding a trailing space "fixed" it. The composer's submit path read the draft from the AUI composer state (`useAuiState(s => s.composer.text)`) and the derived `hasComposerPayload`, both of which lag the contentEditable DOM by a render. On fast typing or IME composition the final keystroke(s) weren't in state yet, so `submitDraft()` saw an empty draft and dropped the message. A trailing space only worked around it by forcing an extra input event that flushed the state. submitDraft() now refreshes draftRef from the editor node and submits/queues based on the live DOM text, and the Enter handler decides the queue-drain vs submit branch from the DOM too. draftRef is already synced on every input event, so this just closes the in-flight-keystroke gap. Fixes NousResearch#39630. Also addresses the "typing + Enter does nothing" reports in NousResearch#39623. * test(desktop): cover Enter-submit from live editor text (NousResearch#39630) Pin the contract that the composer's Enter path reads the live DOM editor text, not the render-lagged composer state: a just-typed message sends even when state hasn't synced; while busy it queues (never drains the queue or cancels); an empty Enter while busy is a no-op; and an empty idle Enter drains the next queued prompt. Faithful DOM-event repro mirroring handleEditorKeyDown + submitDraft.
Co-authored-by: BROCCOLO1D <279959838+BROCCOLO1D@users.noreply.github.com>
…collapse/cap groups (NousResearch#43147) * fix(desktop): prevent sidebar section overlap Use a shared sidebar section scroller only on short windows so sections do not overlap, while preserving per-section scrolling on taller layouts. * fix(desktop): measure section stack for compact sidebar mode Window-height media query kept big windows in compact mode whenever the OS chrome ate into 830px; observe the section stack element instead so compact only engages when the stack is actually short. * refactor(desktop): drive sidebar compact mode with CSS, not JS Replace the matchMedia hook with a `short` (max-height: 830px) Tailwind variant so the per-section scrollers flatten into one shared scroll stack on short windows purely in CSS. Taller windows keep their per-group scrollers and recents virtualization unchanged. * refactor(desktop): pure-CSS two-mode sidebar scroll + collapse/cap groups Drop the JS-measured compaction in favour of a single `compact` height variant (max-height: 768px): - tall: every section is its own capped, independent scroller; Sessions is the lone flex-1 scroller. - short: sections flatten and the stack scrolls as one. Every section is now `shrink-0`, so nothing is squeezed below its content and bled onto a sibling — the root cause of the header overlap (flexbox implied min-size). Sessions keeps its virtualized scroller in short mode only when it's the long list. Non-session groups (messaging, cron) collapse by default — expanded ids persist per platform — and render 3 rows, revealing 10 more on demand. Extract the shared SidebarLoadMoreRow. Stress harness seeds 50 recents to mirror the real first page. * chore(desktop): trim sidebar comments, unify "compact" naming Self-review polish: condense the over-long mode comments, use "compact" consistently (matching the variant) instead of mixing "short", and drop a no-op useCallback around revealMoreMessaging. * chore(desktop): drop dev sidebar stress harness from the PR Remove stress-probe.ts and its main.tsx import — it was a throwaway testing aid, not something to ship.
…Research#43111) Bind session.next/prev to Control+Tab / Control+Shift+Tab with a distinct `ctrl` modifier token (literal Control on macOS — not Cmd, which the OS reserves). Add ^1…^9 positional jumps mirroring profile ⌘1…⌘9. Mac-style interaction: - Quick ^Tab tap jumps on keydown with no HUD (even if Ctrl stays down) - Hold Tab ~220ms, or tap Tab again while Ctrl is held → compact HUD - Ctrl↑ commits the highlight; Esc cancels; rows clickable (^+click safe) - Recency-ordered list snapshotted on open; cycles by stored session id Includes combo.test.ts + session-switcher.test.ts.
…h#43188) The terminal/console titlebar was composed from status marker + model + cwd only; the session's (auto-)title never appeared, even though the TUI already knows it. Change the format to `<marker> <session name> · <model> · <cwd>`, with the session name and cwd each omitted when absent so single-segment titles stay clean. The current session's live title is pulled from the existing session.active_list poll (which already carries each session's current flag and title), so there's no extra round-trip; UiState gains a sessionTitle field updated only when it actually changes, preserving the existing idle-flicker guard. Extract the join logic into a pure composeTabTitle() helper in domain/paths and cover its edge cases (name omitted, cwd omitted, whitespace-only name, marker-only fallback, truncation, boundary length) in paths.test.ts.
Pops a session into a standalone, focused window for side-by-side work. A secondary window loads the renderer at the session route with a ?win=secondary flag (ahead of the HashRouter '#'); it drops the global sidebar plus the install/onboarding overlays and renders a single chat, sharing the one local gateway over WS (no backend duplication). The main process keys windows by sessionId so re-opening focuses the existing one and self-cleans on close. Open it via: - ⌘-click (mac) / ⌃-click (win/linux) a sidebar session — the universal "open in new window" gesture. Archive moves to the ⋯ / right-click menus only, off the easy-to-misfire modifier-click. - "New window" in the session ⋯ and context menus (link-external icon, i18n'd across en/ja/zh/zh-hant). A standalone window has no left rail, so AppShell treats its edge as uncovered and applies the titlebar inset — the chat title clears the macOS traffic lights instead of hiding behind them. Co-authored-by: tim404x <tim404x@users.noreply.github.com>
…ed (NousResearch#43214) A non-empty HERMES_DASHBOARD_PUBLIC_URL / dashboard.public_url value that fails URL validation (overwhelmingly: a missing http(s):// scheme, e.g. "hermes.domain.com") was silently discarded by resolve_public_url(), falling back to reconstructing the OAuth redirect_uri from request headers. Behind a reverse proxy that doesn't forward X-Forwarded-Proto reliably, that yields an http:// callback even though the operator explicitly set the public URL — with no signal as to why (NousResearch#42780). Emit a deduplicated operator-facing WARNING (once per distinct value, since resolve_public_url runs per request) naming the offending value and the required scheme. Turns a silent footgun into a self-diagnosing one; behaviour is otherwise unchanged. Tests assert the warning fires for a scheme-less value, is deduplicated across repeated calls, and stays silent for a valid value — all three fail without the fix.
…NousResearch#43223) The runtime assembled-prompt scan (NousResearch#3968 lineage) selected its pattern tier on has_skills alone. A script-driven, no-skills job injects its script's stdout into the prompt, and that blob was scanned with the STRICT user-prompt pattern set — so any command-shape string in the data feed (e.g. a triage bot ingesting a bug report that quotes `rm -rf /`) hard-blocked the job on every tick. Script output and context_from output are runtime DATA produced by operator-authored code — the same trust class as install-vetted skill markdown, not a user-authored directive prompt. Select the scan tier by what the assembled prompt CONTAINS: when it includes skill content OR injected data, use the looser _scan_cron_skill_assembled set (keeps unambiguous injection directives, drops command-shape patterns, sanitizes invisible unicode instead of blocking). Defense-in-depth is preserved: - The raw user prompt is still strict-scanned at create/update (api_server paths untouched) AND re-scanned strict at runtime even when the looser tier was selected for the data blob. - Plain no-script/no-skills jobs keep the strict scan on the whole assembled prompt. - Injection directives arriving via script stdout still block. Rejected alternative: removing destructive_root_rm from the strict set or a per-job skip_injection_scan flag — both weaken the guard globally.
…ker (NousResearch#42675) (NousResearch#43236) * fix(gateway): auto-start after container restart via planned-stop marker On Docker (s6-overlay), the gateway runs as a dynamically-registered s6 service. When the container stops/restarts/upgrades, s6 sends the gateway a plain SIGTERM. The shutdown path (_stop_impl) ended with an unconditional _update_runtime_status("stopped"), persisting gateway_state=stopped to the volume. container_boot.py reads that on the next boot and only auto-starts gateways whose last state was "running" (_AUTOSTART_STATES) — so after a routine `docker compose up --force-recreate` the gateway stays down and messaging channels silently go dark, with no error surfaced (issue NousResearch#42675). The codebase already distinguishes intentional stops from unexpected signals via the planned-stop marker (write_planned_stop_marker / consume_planned_stop_marker_for_self): `hermes gateway stop`, systemd/launchd ExecStop, and Ctrl+C write a marker before signalling, so the handler classifies them as planned. An unmarked SIGTERM (container/s6 restart, OOM, bare kill) is signal-initiated. This wires that existing classification through to the state persist, rather than adding unreliable signal-source inference: - run.py: GatewayRunner._signal_initiated_shutdown, set in shutdown_signal_handler's unmarked-signal branch. In _stop_impl, a signal-initiated (non-restart) teardown now persists "running" instead of "stopped" — preserving the operator's run-intent and overwriting the mid-shutdown "draining" marker so _AUTOSTART_STATES matches on reboot. Operator stops and restarts persist "stopped" as before. - service_manager.py: S6ServiceManager.stop() now writes the planned-stop marker for the supervised PID (read from s6-svstat) before `s6-svc -d`, so an in-container `hermes gateway stop` is correctly classified as intentional (parity with the systemd/launchd/host stop paths, which already mark). Best-effort: a marker-write failure falls back to the safe signal-initiated path. Tests: shutdown persist-decision table (signal→running, operator→stopped, restart→stopped), s6 stop marker write + svstat PID parse + failure tolerance. The signal→running and s6-marker tests fail without the respective source change. Verified end-to-end against a container built from this branch: an unmarked SIGTERM to the live gateway leaves gateway_state=running (shutdown-context log confirms signal path); existing real container-restart suite still green. * docs(docker): clarify gateway autostart distinguishes operator-stop from container-kill The per-profile-supervision section described the autostart-across-restart contract as "running gateways come back, stopped stay stopped" without spelling out what records 'stopped'. That contract was the source of NousResearch#42675 confusion: users expected a restart to bring the gateway back and it didn't. With the write-side fix, only an explicit `hermes gateway stop` records 'stopped'; container/s6 restart SIGTERMs (incl. image upgrades and unexpected exits) leave the state 'running' so the gateway auto-starts. Make that distinction explicit in both the multi-profile and per-profile-supervision sections. * test(docker): real-restart autostart E2E for NousResearch#42675 Adds test_live_gateway_autostarts_after_real_restart_without_manual_state_stamp: a live s6-supervised gateway is killed by an actual `docker restart` SIGTERM (no manual gateway_state stamp, no planned-stop marker) and must auto-start on the next boot. Exercises the WRITE side of the fix that the existing stamp-based tests bypass. Verified to FAIL against an origin/main image (reconciler logs prior_state=stopped action=registered — the NousResearch#42675 bug) and PASS against the fixed image (prior_state=running action=started).
Browse + install color themes from the VS Code Marketplace straight from Cmd-K and Settings → Appearance. The Electron main process resolves the extension, unzips the .vsix with a hand-rolled zip reader (zlib only, no new deps), and hands back the raw theme JSON; the renderer converts it to a DesktopTheme with a small seed → color-mix mapping. - Folds an extension's light + dark variants into one theme family, so the light/dark toggle switches Solarized/GitHub variants and installing in dark mode stays dark. - Guarantees accent contrast (WCAG AA) so imported sidebar labels read instead of vanishing into the surface. - Filters icon/product-icon packs out of the Themes-category search. - "Install theme…" lives atop the Cmd-K theme picker; imports fold into the Light/Dark groups by the modes they support.
NousResearch#42521) * refactor(desktop): dock terminal under chat and simplify file rail Keep the right rail focused on file browsing while moving the persistent terminal into the chat column bottom slot, and make terminal colors follow the active light/dark mode instead of a fixed Solarized palette. * fix(desktop): make the terminal a resizable, themed side pane - Move the terminal into a resizable pane (viewport-% widths) that shares <main>'s stacking context, so its drag handle no longer sits under the fixed terminal overlay; works on either rail side. - Restore +x on node-pty's spawn-helper before the first spawn to fix "posix_spawnp failed" on macOS prebuilds (real cause; drop the redundant shell-candidate retry loop). - Gate terminal open/fit/start on document.fonts.ready and strip leading blank rows (re-armed before the resize Ctrl-L redraw) so the prompt sits flush at the top with no starship add_newline gap. - Inherit the app editor-surface color as the terminal background. - Bind Ctrl+` (⌃` on macOS) to toggle the terminal; add a palette entry. * feat(desktop): show platform hotkey hints in the command palette - Render each palette item's live binding as a <KbdGroup> hint via a new comboTokens() helper (mac shows ⌘/⌃/⌥/⇧, every other platform shows Ctrl/Alt/Shift — never a ⌘ on PC). - Default the terminal toggle to ⌘` / Ctrl+` (the ~ key) on both platforms. - Drop the hardcoded (⌘⏎) baked into the composer steer tooltip; render it platform-aware with formatCombo instead. * fix(desktop): drop the active check on the command-palette terminal item * fix(desktop): remove active/check states from the command palette * fix(desktop): allow ⌥/Shift-drag selection over mouse-mode TUIs Full-screen apps (hermes --tui, vim) enable mouse reporting, so a plain drag can't select text and ⌘/Ctrl+L (add-selection-to-chat) had nothing to send. Enable macOptionClickForcesSelection so ⌥-drag on macOS (Shift elsewhere) forces a native selection over mouse-mode apps. * feat(desktop): tell the in-pane agent it's embedded in the GUI Set HERMES_DESKTOP_TERMINAL=1 on the terminal pane's shell env and surface it in build_environment_hints, so a hermes/--tui launched inside the pane knows it's next to the GUI chat and that ⌥/Shift-drag + ⌘/Ctrl+L sends a selection to the composer. Distinct from HERMES_DESKTOP (agent backend). * refactor(desktop): drop the redundant Ctrl+` terminal-toggle fallback The toggle now ships as mod+` on both platforms, so the standard combo index handles it — the bespoke fallback (and its stale 'old default' comment) is dead weight. * fix(desktop): read live terminal selection for ⌘/Ctrl+L A redraw-heavy TUI (spinners/clocks) outruns onSelectionChange, leaving the React selection state empty so the state-gated shortcut listener never attached and ⌘L no-op'd. Always listen and read xterm's live selection (with a native fallback) at press time; only swallow the key when there's text to send. Drops the now-redundant custom key handler. * feat(desktop): make any agent aware it's in the Hermes desktop GUI Generalize the runtime-surface hint: fire for HERMES_DESKTOP (the backend powering the GUI chat) as well as HERMES_DESKTOP_TERMINAL (a hermes in the embedded terminal pane), so it's about being inside the desktop GUI, not about being a TUI. The terminal-pane selection note stays pane-specific. * feat(desktop): give the GUI agent a read_terminal tool The in-app terminal buffer lives in the renderer (xterm), so expose it to the chat agent over the same blocking bridge clarify uses: read_terminal emits terminal.read.request, the renderer serializes the buffer (visible screen by default, or a start_line/count range against total_lines) and answers terminal.read.respond. Gated to the GUI via HERMES_DESKTOP. Also restores the flipped-layout titlebar inset fix (app-shell + desktop-controller) for terminal/preview rails at the window's left edge. * chore(desktop): trim read_terminal comments * feat(desktop): add a terminal toggle to the statusbar The file rail lost its terminal icon, leaving ⌘` and the command palette as the only ways in. Add a one-click toggle to the statusbar's left cluster, mirroring the command-center item: it reads $terminalTakeover so it lights up while the pane is open and stays in sync with the hotkey, and is gated to chat view (the only place the pane can show). * fix(desktop): relabel the terminal header button to what it does The in-pane button claimed a focus/split fullscreen toggle ("Focus terminal view" / "Return to split view", screen-full/normal icons), but the terminal is just a resizable side pane — there's no fullscreen. The button only mounts while the pane is open, so the focus branch was dead and clicking it merely closed the terminal. Relabel to "Hide terminal" with a close icon, drop the dead conditional and the now-unused takeover read. * fix(desktop): move the terminal toggle next to the version item Relocate it from the left cluster to the right of the statusbar, just left of the client version item. * feat(desktop): default the terminal to PowerShell on Windows Prefer pwsh (7+) then Windows PowerShell 5.1 over cmd.exe, falling back to comspec only when neither is present. -NoLogo drops the startup banner so the prompt sits flush like the POSIX shells. * feat(desktop): show a persistent divider on the terminal pane The resize sash only painted on hover, so the terminal/chat boundary was invisible at rest. Add an opt-in `divider` prop to Pane that paints a thin resting hairline on the resize edge (side-aware, so it tracks the rail when the layout flips) and enable it on the terminal pane. * refactor(desktop): resolve the terminal shell instead of hardcoding it Make shell selection a real resolver: an explicit override wins (HERMES_DESKTOP_SHELL on both platforms, $SHELL on POSIX), otherwise auto-detect the best installed shell — pwsh > Windows PowerShell 5.1 > cmd on Windows, zsh > bash > sh on POSIX. A shared shellSpecFor() picks the interactive flags by family, so an overridden bash/pwsh/cmd all launch correctly. * fix(desktop): repaint the terminal on light/dark switch Setting term.options.theme updated colors for the DOM renderer but not the WebGL one, which caches glyph colors in a texture atlas — so already-drawn cells kept their old palette after a mode switch. Hold the WebglAddon in a ref and clear its atlas when the theme changes. * fix(desktop): match the terminal palette to VS Code Light+/Dark+ Adopt VS Code's exact default ANSI palette (the terminalColorRegistry defaults), enable minimumContrastRatio: 4.5 so foregrounds are clamped against the background the way the integrated terminal does, and key the light/dark choice off renderedMode (the painted surface) instead of resolvedMode so it can't invert. The canvas + inset paint the live skin surface (--ui-editor-surface-background) so the terminal blends with the app and follows light/dark, while the contrast clamp keeps colors crisp. * fix(desktop): tighten command palette search to substring matching cmdk's default fuzzy scorer matched anything with the query letters scattered across an item, so e.g. "color" never narrowed to color entries. Add a substring filter: every typed word must literally appear in an item's value/keywords, keeping results tight and predictable. * fix(desktop): blend the terminal header into the skin surface The persistent-terminal overlay painted the static palette background (#1e1e1e/#ffffff), so the transparent header strip revealed a near-black slab above the surface-colored body. Paint the overlay with the live --ui-editor-surface-background so header and body read as one pane. * fix(desktop): re-resolve the terminal surface on skin switch The canvas surface only re-resolved on light/dark change, so switching skins at the same mode left the WebGL canvas painted with the old tint until reload. Key the resolve off themeName too. Also trim the palette comments. * chore(desktop): drop redundant terminal theming header comment
…-themes # Conflicts: # apps/desktop/electron/main.cjs # apps/desktop/src/app/command-palette/index.tsx # apps/desktop/src/themes/context.tsx
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
NousResearch#42641) AGENTS.md was almost entirely how-to/mechanics with the want/don't-want guidance implicit and scattered. Adds a single authoritative intent layer near the top, calibrated against what actually merges and what actually gets rejected. - 'What Hermes Is': framing + the two properties that drive design (prompt-cache integrity, narrow-waist core). - 'Contribution Rubric': dual-purpose intent doc — (1) for humans/own work: what gets merged vs rejected; (2) for the triage sweeper: when a PR is safe to close on the three allowed reasons AND when NOT to close one. Taste-based 'won't implement / out of scope' closes stay human-only by design. - 'What we want' calibrated against the last ~55 merges: fix real bugs well, expand reach at the edges (platforms/channels/providers/models/desktop — large features land routinely), refactor god-files into clean modules, keep the CORE narrow. 'Expansive at the edges, conservative at the waist.' - 'What we don't want': speculative hooks, .env-for-non-secrets, needless core tools, lazy-read escape hatches, feature-destroying fixes, ungated telemetry, change-detector tests, core-touching plugins. - 'Before you call it a bug — verify the premise (and when NOT to close)': distilled from real closes (NousResearch#41741 intentional-design-not-a-gap, NousResearch#41610 wrong-premise, NousResearch#42327 fix-never-executes, NousResearch#42393 deliberate-omission, NousResearch#41999 overreach). Doubles as sweeper guidance to avoid wrongly closing legitimate PRs. - 'The Footprint Ladder' (core-tool decision): extend > CLI+skill > gated tool > plugin > MCP server in the catalog > new core tool (last resort). Trim: 'Adding New Tools' intro points at the ladder. Detailed mechanics stay where readers need them.
…Ctrl-Tab HUDs Imported VS Code themes now carry their integrated-terminal ANSI palette (`terminal.ansi*`), keyed to the painted variant (terminal / darkTerminal). The terminal adopts it when the full base-8 set is present and keeps its VS Code defaults otherwise; withSurface still owns the background, so the pane stays translucent. Pull the command palette and session switcher into a shared top-center HUD (`floating-hud.ts`): no dim/blur backdrop, one compact text + item-padding size, sidebar-label-style section headers (brand-tinted, uppercase), and the themed portal scrollbar.
…est files (NousResearch#42994) The per-file test runner re-runs a file once when pytest exits 4 ("file or directory not found") while the file exists on disk — a transient seen on loaded shared CI runners where the planner collects a file (--collect-only counts its tests) but the per-file subprocess fails to stat it moments later. A single immediate retry could land in the same brief high-load window and fail again, and the retry was gated on one Path.exists() check that can itself be a flaky stat under that load — so a freshly-added test file that LPT pins to one shard would deterministically red that shard on every run (no actual test failure; the file just never executes). - Extract the subprocess spawn/communicate/process-tree-kill logic into a shared _spawn_pytest_once() helper (removes ~90 lines of duplication between the primary run and the retry). - Replace the single-shot retry with a bounded backoff loop (_EXIT4_RETRY_ATTEMPTS, escalating sleep) that re-runs while the file is present on disk. - Add _file_present() which re-checks existence across a few spaced stats, so a single flaky negative stat doesn't wrongly conclude the file is missing. A genuinely-missing file (typo/deleted) still fails fast — exit 4 is not swallowed when the file truly does not exist. - Tests: transient-then-pass recovery, genuinely-missing fails fast with no retry, give-up after max attempts, and _file_present transient/missing cases.
…l-skills — Anthropic classifier (NousResearch#43221) * chore(skills): remove red-team skills (godmode, obliteratus) from bundled catalog Anthropic's output classifier on claude-fable-5 (and likely other Claude models served through it) intermittently returns empty content for sessions whose system prompt advertises these skills. The bundled skills-catalog block is injected into every session's system prompt, so the descriptions - red-teaming/godmode 'Jailbreak LLMs: Parseltongue, GODMODE, ULTRAPLINIAN' - mlops/inference/obliteratus 'OBLITERATUS: abliterate LLM refusals (diff-in-means)' trip the classifier on EVERY session regardless of which skill is actually loaded, killing unrelated legitimate work (PR review, codebase audits, etc.). Measured impact (controlled, interleaved A/B, claude-fable-5 via OpenRouter, prompts differing only by the ~204 chars of these catalog lines, N=20 each): catalog lines present -> 19/20 (95%) blocked catalog lines absent -> 5/20 (25%) blocked Removing them ~quartered the block rate. Rewording the descriptions was not enough; the skills must leave the bundled catalog. - Delete skills/red-teaming/godmode and skills/mlops/inference/obliteratus - Drop their generated doc pages + catalog/sidebar entries (EN + zh-Hans) - Drop the godmode hand-written-page exception in generate-skill-docs.py * chore(skills): relocate godmode + obliteratus to optional-skills Rather than deleting outright, move both into optional-skills/ so they remain installable via `hermes skills install` while leaving the always-injected bundled catalog (which is what tripped Anthropic's classifier). - optional-skills/security/godmode (was skills/red-teaming/godmode) - optional-skills/mlops/obliteratus (was skills/mlops/inference/obliteratus) - regenerate optional-skills catalog + sidebar entries
concurrently 9 had a critical vuln dependency, react-compiler eslint plugin is built into react-hooks eslint plugin as of https://react.dev/blog/2025/10/07/react-compiler-1
NousResearch#39084) * feat(profiles): extend create endpoint for full profile-builder (model + MCPs + skills) Backend foundation for the dashboard profile builder. Extends POST /api/profiles to accept, in one call, everything a profile needs beyond name/clone: - mcp_servers[] -> written into the new profile's config.yaml - keep_skills[] -> replace-semantics: disable every seeded skill not kept - hub_skills[] -> async install via 'hermes -p <name> skills install <id>' All applied best-effort AFTER the profile dir exists, so a hiccup in any one never 500s the create. Model/MCP/keep-skills writes are profile-scoped via the HERMES_HOME context override (same mechanism as the existing _write_profile_model). Hub installs go through a subprocess scoped with -p because skills_hub.SKILLS_DIR is import-time-bound and the runtime override can't redirect it. Adds two helpers (_write_profile_mcp_servers, _disable_unselected_skills) and a TestClient test asserting all four paths land in the NEW profile's config and the hub spawn is scoped to it. Design doc at docs/design/profile-builder.md. * feat(dashboard): full-featured profile builder page Adds a dedicated /profiles/new builder that composes everything a profile needs into one stepped create flow, reusing the existing Models/Skills/MCP data paths instead of duplicating them: - Identity name + description - Model provider+model picker (api.getModelOptions) - Skills keep-which-built-in/optional (replace semantics, default = full bundle) + skills-hub search/add (api.getSkills, searchSkillsHub) - MCPs add HTTP/stdio servers inline - Review blueprint -> single POST /api/profiles create Nothing writes until Create; the one call commits model+MCPs+skill selection and spawns hub-skill installs (reported in the success toast). ProfilesPage header gets a 'Build' button (full builder) alongside 'Create' (quick modal). Route is page-only (not in the sidebar nav). Verified with vite build (2258 modules, green).
…ut transport design The fork consolidation deleted upstream's drop-sentinel + close_on_disconnect teardown that tests/test_tui_gateway_ws.py pins. Reconcile the two designs: keep the fork's FanoutTransport (channel co-viewing), park last-detached sessions on _detached_ws_transport instead of real stdio, reap flagged sessions immediately through a restored _close_session_by_id funnel, and fold the slash-worker close into _finalize_session (NousResearch#38095 chokepoint semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and worker-restart guard More casualties of the fork consolidation overwrite, restored from upstream and reconciled with fork features: - gateway/run.py: max_concurrent_sessions claim (_claim_active_session_slot + cross-process lease via hermes_cli.active_sessions) before the pending sentinel, lease release in _release_running_agent_state, lease map init + shutdown clear. - tui_gateway/server.py: _terminal_task_cwd (SSH-backend cwd resolution), _restart_slash_worker(sid, session) with the _attach_worker create/close race guard, session.active_list _finalized liveness filter, drop duplicate session_id row field (id is canonical; fork presence test updated). - tests: fork-era monkeypatches aligned to the two-arg worker-restart signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… zoom persistence, port statusbar modifier support - electron/main.cjs: re-add releaseBackendLock (uninstall path called an undefined symbol post-merge), route menu/shortcut zoom through setAndPersistZoomLevel and restore persisted zoom on did-finish-load. - statusbar-controls: StatusbarSelectModifiers type + shift-key pass-through so the upstream global-yolo shift-click works with the fork controls. - hermes.ts: getCronJobRuns for the upstream cron quick-peek section; session-row import dedup; chatOpen optional for the fork controller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fork consolidation; finish fork-feature ports Per-cluster restoration with the test suite as the oracle, after comparing the merged tree's failures against a pristine-upstream run in the same environment (14 file-level deltas, now zero): - gateway/run.py: upstream wholesale (fork's monolith had undone the mixin decomposition; both real fork deltas re-applied — voice_ack_callback **kwargs; the custom-providers context-length fix exists upstream). - agent/conversation_loop.py + turn_context.py: upstream structure with the fork features regrafted at their new homes — sender_device attribution (#131), preflight token-usage emission + compression-complete status and live-estimate snapshots (#126). - agent/chat_completion_helpers.py: upstream wholesale (brings the second partial-stream-stub routing site and the NousResearch#6600 cancellation fix). - agent/tool_executor.py: usage= kwarg on tool start/complete callbacks now falls back to the bare 3-arg form for legacy receivers. - tools/approval.py: upstream's resolved-HERMES_HOME rewrite + normalize steps restored alongside the fork's self-host kill guard (#128). - hermes_cli/main.py: desktop install-identity stale-build cluster and the post-subcommand global-flag hoister ported from fork main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the turn-start persist hermes_logging.py: upstream wholesale — the fork copy predated _safe_stderr, which upstream's gateway runner imports (3 startup tests). test_413_compression: upstream's turn_context adds a crash-resilience persist of the inbound user turn BEFORE any 413 can occur, legitimately carrying the pre-compression history. The NousResearch#7001 invariant — persists after mid-loop compression pass None so the flush targets the compression-created session — is now pinned on the post-compression persists instead of all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
39 |
unresolved-import |
15 |
invalid-argument-type |
15 |
invalid-assignment |
6 |
unsupported-operator |
6 |
invalid-method-override |
5 |
unresolved-reference |
2 |
call-non-callable |
2 |
not-subscriptable |
1 |
invalid-parameter-default |
1 |
First entries
tests/hermes_cli/test_web_server_files.py:6: [unresolved-import] unresolved-import: Cannot resolve imported module `starlette.testclient`
tests/hermes_cli/test_web_server.py:834: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `None | Connection`
hermes_state.py:836: [unresolved-attribute] unresolved-attribute: Attribute `rollback` is not defined on `None` in union `None | Connection`
gateway/slash_commands.py:1201: [invalid-assignment] invalid-assignment: Object of type `dict[Unknown, Unknown]` is not assignable to attribute `_pending_model_notes` on type `Self@_handle_model_command & ~<Protocol with members '_pending_model_notes'>`
tools/skill_manager_tool.py:879: [invalid-argument-type] invalid-argument-type: Argument to function `skill_manage` is incorrect: Expected `str`, found `Any | None`
tests/test_sender_attribution.py:86: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `None | Connection`
gateway/slash_commands.py:2034: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_memory_command` has no attribute `_session_key_for_source`
tests/gateway/test_telegram_topic_mode.py:1333: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `None | Connection`
tests/tools/test_write_approval.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/providers/test_provider_profiles.py:304: [unresolved-attribute] unresolved-attribute: Attribute `_anthropic_reasoning_is_mandatory` is not defined on `None` in union `ModuleType | None`
plugins/platforms/simplex/adapter.py:1033: [invalid-method-override] invalid-method-override: Invalid override of method `send_voice`: Definition is incompatible with `BasePlatformAdapter.send_voice`
hermes_logging.py:82: [invalid-assignment] invalid-assignment: Object of type `() -> None` is not assignable to attribute `close` of type `def close(self) -> None`
tests/test_sender_attribution.py:69: [unresolved-attribute] unresolved-attribute: Attribute `executescript` is not defined on `None` in union `None | Connection`
tests/tools/test_write_approval.py:191: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["No pending memory"]` and `str | None`
tests/test_sender_attribution.py:76: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `None | Connection`
gateway/slash_commands.py:2100: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_skills_command` has no attribute `_evict_cached_agent`
tests/gateway/test_telegram_topic_mode.py:1100: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `None | Connection`
optional-skills/security/godmode/scripts/godmode_race.py:27: [unresolved-import] unresolved-import: Cannot resolve imported module `openai`
tests/tools/test_write_approval.py:177: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/hermes_cli/test_web_server_files.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/slash_commands.py:60: [unresolved-attribute] unresolved-attribute: Object of type `Self@_typed_command_prefix_for` has no attribute `adapters`
tests/hermes_cli/test_kanban_notify.py:465: [invalid-argument-type] invalid-argument-type: Argument to function `GatewaySlashCommandsMixin._handle_kanban_command` is incorrect: Expected `MessageEvent`, found `SimpleNamespace`
tests/tools/test_write_approval.py:240: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["off"]` and `str | None`
tests/hermes_state/test_resolve_resume_session_id.py:30: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `None | Connection`
optional-skills/security/godmode/scripts/auto_jailbreak.py:620: [unresolved-reference] unresolved-reference: Name `score_response` used when not defined
... and 67 more
✅ Fixed issues (71):
| Rule | Count |
|---|---|
unresolved-attribute |
39 |
invalid-argument-type |
20 |
unresolved-import |
3 |
unresolved-reference |
3 |
call-non-callable |
2 |
invalid-return-type |
1 |
invalid-assignment |
1 |
invalid-type-form |
1 |
invalid-parameter-default |
1 |
First entries
gateway/run.py:11596: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `str`, found `str | None`
gateway/run.py:11297: [invalid-argument-type] invalid-argument-type: Argument to function `switch_model` is incorrect: Expected `dict[Unknown, Unknown]`, found `None | Unknown`
gateway/run.py:11959: [invalid-argument-type] invalid-argument-type: Argument to function `_home_target_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/main.py:6018: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def probe_gemini_tier(api_key: str, base_url: str = "https://generativelanguage.googleapis.com/v1beta", *, model: str = "gemini-2.5-flash", timeout: int | float = ...) -> str`
tests/hermes_cli/test_resolve_last_session.py:148: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
agent/conversation_loop.py:820: [invalid-type-form] invalid-type-form: Type annotations are not allowed on this attribute expression
skills/red-teaming/godmode/scripts/auto_jailbreak.py:527: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `list[str]` in union `list[str] | dict[str, str] | dict[Unknown, Unknown]`
gateway/run.py:13909: [unresolved-attribute] unresolved-attribute: Attribute `list_sessions_rich` is not defined on `None` in union `None | SessionDB`
hermes_state.py:5043: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
tests/test_tui_gateway_ws.py:87: [unresolved-attribute] unresolved-attribute: Module `tui_gateway.server` has no member `_detached_ws_transport`
hermes_state.py:775: [unresolved-attribute] unresolved-attribute: Attribute `cursor` is not defined on `None` in union `Connection | None`
gateway/run.py:7193: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore.is_approved` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
plugins/platforms/simplex/adapter.py:308: [invalid-argument-type] invalid-argument-type: Argument to bound method `SimplexAdapter._handle_new_chat_item` is incorrect: Expected `dict[Unknown, Unknown]`, found `Unknown | None | dict[Unknown, Unknown]`
tests/tools/test_session_search.py:459: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
hermes_state.py:598: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `Connection`, found `Connection | None`
gateway/run.py:11960: [invalid-argument-type] invalid-argument-type: Argument to function `_home_thread_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
agent/conversation_loop.py:4184: [unresolved-attribute] unresolved-attribute: Object of type `None` has no attribute `code`
hermes_cli/main.py:4831: [invalid-argument-type] invalid-argument-type: Argument to function `fetch_api_models` is incorrect: Expected `str | None`, found `int | float`
tests/hermes_cli/test_resolve_last_session.py:152: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `Connection | None`
gateway/run.py:11117: [invalid-argument-type] invalid-argument-type: Argument to function `list_picker_providers` is incorrect: Expected `dict[Unknown, Unknown]`, found `None | Unknown`
tests/hermes_cli/test_web_server.py:830: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
tests/gateway/test_version_command.py:11: [invalid-argument-type] invalid-argument-type: Argument to function `GatewayRunner._handle_version_command` is incorrect: Expected `MessageEvent`, found `None`
agent/conversation_loop.py:4826: [unresolved-attribute] unresolved-attribute: Attribute `rstrip` is not defined on `None` in union `None | Unknown | str`
agent/conversation_loop.py:4972: [unresolved-attribute] unresolved-attribute: Object of type `None` has no attribute `to_metadata`
tests/hermes_cli/test_kanban_notify.py:465: [invalid-argument-type] invalid-argument-type: Argument to function `GatewayRunner._handle_kanban_command` is incorrect: Expected `MessageEvent`, found `SimpleNamespace`
... and 46 more
Unchanged: 5480 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…in reconciled last-detach semantics tools/transcription_tools.py + tools/environments/ssh.py: upstream wholesale — the merge had kept fork copies predating upstream's stdin=DEVNULL hardening, tripping test_subprocess_stdin_guard on CI (only fork history on both files was the consolidation overwrite). tests/tui_gateway/test_concurrent_attach.py: the last-detach test pinned the fork's old revert-to-stdio behavior; the sync reconciles on upstream's drop sentinel (no stdio frame leaks in the in-process gateway, orphan-reap eligibility), so the test now pins the sentinel + verifies reattach replaces it cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mes-ink typings scripts/release.py: AUTHOR_MAP entries for the two upstream contributors the sync introduces (konsisumer noreply form, maplestoryjuni222 → BROCCOLO1D) — the attribution gate scans every PR-introduced author. ui-tui/packages/hermes-ink: upstream wholesale (the merge kept a fork copy predating upstream's typed child_process usage; only fork history was the consolidation overwrite). ui-tui + web + bootstrap-installer + shared all typecheck green locally under upstream's new typecheck workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mplex protocol adapter cmd_update resolves the target branch's configured tracking remote (_resolve_update_target, for-each-ref %(upstream:short)) and fetches + compares against that ref instead of hard-coded origin/<branch> — the managed desktop install keeps main tracking the fork remote while origin points upstream (fork test test_cmd_update_uses_target_branch_tracking_remote pins it). plugins/platforms/simplex/adapter.py taken from upstream (the merge kept a fork copy predating the /_send json protocol; 9ac-only fork history). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_resolve_update_target (for-each-ref %(upstream:short)) picks the remote, branch, and compare ref; non-origin tracking remotes get fetched and all checkout/rev-list/reset comparisons use the resolved ref instead of hard-coded origin/<branch>. Managed desktop installs keep main tracking the fork remote while origin points upstream — the old behavior reported 'Already up to date' against the wrong remote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r/web-ui tests from upstream The ff-only pull in _cmd_update_impl now targets the resolved update_remote/update_remote_branch (completing the tracking-remote flow); tests/hermes_cli/test_web_ui_build.py + test_cmd_update.py taken from upstream — the merge had kept stale copies asserting the pre-workspace npm install commands (9ac-only fork history on both). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dapt upstream test constants; add TEMP CI probe 647b1fb swept the compression-threshold default to 0.85 deliberately (matching hermes_cli/config.py); the upstream-era tail-budget test constants now compute from that default (200K→68K, 1M→340K). tests/test_zz_ci_probe_tmp.py is a TEMPORARY always-green probe that prints disk hashes vs imported-object reality for the two surfaces whose CI failures contradict the checked-out tree; it gets deleted once the divergence is identified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…obe now surfaces diagnostics Shard failures rotated with slice composition across CI rounds (approval home-rewrite tests, active_list row shape, voice source checks) and reproduced in no other environment — the signature of one test file mutating the shared real ~/.hermes and poisoning every later file in its shard. Each pytest subprocess now gets a fresh temporary HERMES_HOME, which is the isolation this per-file runner exists to provide. The TEMP probe now raises with its diagnostics (pytest swallows passing tests' output) — still slated for deletion before merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe findings (round 6): CI's checkout, imports, and inspected sources are all healthy on the failing shard's commit — finally-unlink present, active_list row shape correct, no stale artifacts. Combined with an empty repo-mutation shim log and a clean local replay of CI's exact shard-5 file set, the remaining rotating shard-5 failures are CI-machine-coupled (parallel-worker state race), tracked on MeshBoard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jun 10, 2026
OmarB97
added a commit
that referenced
this pull request
Jun 10, 2026
… crash on launch) (#156) electron/main.cjs:42 requires './desktop-uninstall.cjs' (modeRemovesUserData, resolveRemovableAppPath, shouldRemoveAppBundle, uninstallArgsForMode), and package.json's test:desktop:platforms references desktop-uninstall.test.cjs — but both files were dropped during the #152 sync merge while their references survived. The packaged app crashed on launch: "Cannot find module './desktop-uninstall.cjs'" at main.cjs:42. Restored both verbatim from upstream/main (the uninstall feature, NousResearch#40355). node --test: 19/19 pass; main.cjs resolves past the require. Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
OmarB97
added a commit
that referenced
this pull request
Jun 10, 2026
…ad of resurrecting it (#159) Deleting a session whose backend row no longer exists (deleted on another device, or a remote profile's session this device only remembers via a pin) 404s "Session not found" — and removeSession's catch rolled back the optimistic removal INCLUDING the pin. Result: a ghost pinned row that renders forever and can never be deleted (every retry 404s and resurrects it again); the operator hit exactly this with a 3-day-old pinned <think> session. Treat "Session not found" as already-deleted: keep the optimistic removal (row, total, pin) and finish local cleanup instead of rolling back. Other delete failures still roll back and surface the error. Also sweeps two pre-existing lint errors in this file's import block (#152-era debt). Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Why
Fork main is 64+ commits ahead / 88+ behind upstream and every sync attempt has aborted on a 26-file conflict since 2026-06-07, so drift compounds daily (tracked:
hermes-fork-resolve-85-commit-upstream-sync-25-file). Worse, the June 7-9 consolidation merges overwrote several files with stale pre-decomposition copies, silently deleting upstream code that was already in the merge base — git records those as deliberate fork deletions, so no future merge can restore them. That deleted code is exactly why the fork's Tests workflow is red on main itself (all 6 shards): upstream tests pin upstream code the fork lost (tui_gateway.server._detached_ws_transport, the gateway session-capacity gate,hermes_logging._safe_stderr, the second partial-stream-stub routing site, …).What changed
upstream/mainwith all 26 conflicts resolved (workflows, electron main, package.json, sidebar tsx cluster, i18n, gateway platforms, hermes_cli config/main, web_server, release script, ui-tui, tests) plus a follow-up absorb of fork PRs feat(desktop): attach to sessions on other devices — Live section + remote dial (Phase 2b/2) #139–feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint #146.gateway/run.py,hermes_cli/main.py,agent/conversation_loop.py,agent/chat_completion_helpers.py,hermes_logging.pyare upstream's decomposed versions; re-applied on top: the updater's merge+abort fork sync (fix: abort conflicted upstream-sync merge so update never leaves a broken tree #136 semantics), voice_ack **kwargs, sender_device attribution (feat(gateway): Phase 2 foundations — prompt sender_device param + advertised presence endpoints #131) now inagent/turn_context.py, preflight token-usage emission + compression-complete status + live-estimate snapshots (fix(context): live mid-turn context bar + visible auto-compression status #126), the desktop install-identity stale-build cluster and post-subcommand flag hoister, and theusage=tool-callback kwarg with a 3-arg fallback for legacy receivers.close_on_disconnectreap through a restored_close_session_by_idfunnel, fanout-aware_close_sessions_for_transport, and worker-close folded into_finalize_session;tools/approval.pycarries both the fork's self-host kill guard (feat(approval): block agents from killing their own gateway/host process #128) and upstream's resolved-HERMES_HOME rewrite; the statusbar hook keeps upstream's client/backend split + global-yolo shift-click with the fork's+rebuildpill and activity-status item grafted in;_config_versionlands on upstream's 29 (the fork's 27 was a past bad-resolution downgrade).session_idrow field dropped (idis canonical), 413-compression tests now pin the post-compression persists (upstream's turn-start crash-resilience persist legitimately precedes compression).How to review
e866446df,820131b27) for resolution choices — every conflicted file's resolution rationale is in the four fix-commit messages on this branch.4493902c5ws-teardown,647f64252capacity gate,93acdc56acluster restorations,1a0…logging/tests) — each names the lost-upstream symptom and the fork features preserved._close_sessions_for_transport's docstring — the fanout-aware semantics are the one genuinely novel piece.apps/desktoptypechecks and builds; sidebar/statusbar diffs preserve the operator's shipped UI (fix(desktop): restore compact sidebar session-row density (38px → 26px rows) #138–feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint #146) over upstream's where they collide.Evidence
run_tests_parallel.py, all slices) on this branch vs a pristineupstream/maincheckout in the SAME macOS environment: the branch's failing-file set is a subset of upstream's own environment baseline (15 files, env-shaped: audio/voice deps, /tmp symlink semantics, hermetic-conftest quirks that CI's ubuntu runners don't hit). Zero fork-introduced failures remain; before this branch, 14 file clusters failed beyond baseline.tsc -p . --noEmitgreen; production vite build green;test:desktop:platformsnode suite 122/122; vitest renderer failures byte-identical to fork main's 7 (peer PRs fix(desktop): recoverable boot error after prolonged gateway drop (escape hatch) #147/fix(desktop): restore sleep/wake session recovery + repair 3 stale renderer tests #148 address those independently).ruff checkgreen on every changed python file.Verification
Risks / gaps
hermes-port-upstream-pane-collapse-system(see Collaborators note in task).use-prompt-actions.ts/use-gateway-bootregions if they merge first — whichever lands second absorbs; this branch already contains upstream's sleep/wake retry that fix(desktop): restore sleep/wake session recovery + repair 3 stale renderer tests #148 re-implements, so the resolution is mechanical — coordinated via the task record.cancellednix (macos) + sticky-comment 401 reds on PR checks are pre-existing infra noise, fixed separately by ci(nix): keep the nix job green when the sticky lockfile comment can't post #149 — covered by taskfork-ci-nix-job-fails-every-pr-at-sticky-pr-comment._config_version27→29 runs upstream's 28→29 write_approval rename on first boot after deploy — low risk, the migration is upstream-shipped and idempotent.Collaborators