Skip to content

merge upstream (88+ commits): resolve the 26-file conflict, restore consolidation-lost code, green the Tests workflow#152

Merged
OmarB97 merged 108 commits into
mainfrom
sync/upstream-20260610-v2
Jun 10, 2026
Merged

merge upstream (88+ commits): resolve the 26-file conflict, restore consolidation-lost code, green the Tests workflow#152
OmarB97 merged 108 commits into
mainfrom
sync/upstream-20260610-v2

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

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

How to review

  1. Sanity-check the merge commits (e866446df, 820131b27) for resolution choices — every conflicted file's resolution rationale is in the four fix-commit messages on this branch.
  2. Review the four restoration commits in order (4493902c5 ws-teardown, 647f64252 capacity gate, 93acdc56a cluster restorations, 1a0… logging/tests) — each names the lost-upstream symptom and the fork features preserved.
  3. For the tui_gateway teardown reconciliation, read _close_sessions_for_transport's docstring — the fanout-aware semantics are the one genuinely novel piece.
  4. Renderer: apps/desktop typechecks and builds; sidebar/statusbar diffs preserve the operator's shipped UI (fix(desktop): restore compact sidebar session-row density (38px → 26px rows) #138feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint #146) over upstream's where they collide.

Evidence

  • Full per-file suite (CI's run_tests_parallel.py, all slices) on this branch vs a pristine upstream/main checkout 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.
  • Oracle cluster green: tests/test_tui_gateway_server.py + test_tui_gateway_ws.py + gateway/test_max_concurrent_sessions.py → 241 passed.
  • Desktop: tsc -p . --noEmit green; production vite build green; test:desktop:platforms node 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 check green on every changed python file.

Verification

  • All counts above re-run on the final absorbed head (820131b): oracle 241 passed, TSC green, build green.
  • The decisive proof is this PR's own Tests workflow: 6/6 shards green would be the first green Tests run on a fork head since the consolidation. If any shard reds on ubuntu-only behavior, I iterate before merge.
  • Post-merge: rebuild + swap /Applications/Hermes.app and run a live smoke (app boots, sessions list, prompt round-trip) — recorded on the MeshBoard task.

Risks / gaps

Collaborators

  • @OmarB97 (operator)
  • Claude Fable 5 (ko-mac.claude, upstream-sync instance)

teknium1 and others added 30 commits June 9, 2026 14:45
…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
ethernet8023 and others added 13 commits June 10, 2026 11:59
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>
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

🔎 Lint report: sync/upstream-20260610-v2 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10647 on HEAD, 10597 on base (🆕 +50)

🆕 New issues (92):

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.

Omar Baradei and others added 9 commits June 10, 2026 11:47
…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>
@OmarB97 OmarB97 merged commit 56875cb into main Jun 10, 2026
26 of 27 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.