Commit graph

2295 commits

Author SHA1 Message Date
Teknium
37e648128b fix(picker): fold live bare k3 wire id into curated kimi-k3 row
Follow-up to the salvaged #67409 search aliases: with kimi-k3 now in
the curated kimi-coding list (#68108), a Coding Plan key rendered TWO
rows for one model — curated 'kimi-k3' plus live-discovered bare 'k3'
(merge dedup was exact-string). Add model_alias_canonical() derived
from the same alias table and use it as the merge dedup key, so the
curated public slug wins and live-only models still surface.
2026-07-26 21:22:40 -07:00
HexLab98
39902f1888 test(models): cover kimi search alias for Kimi Coding k3
Assert the picker haystack keeps ordinary ids unchanged, surfaces wire
id k3 for "kimi"/"k3" queries, and accept search_labels in curses mocks.
2026-07-26 21:22:40 -07:00
Teknium
f9cd577915 feat(approvals): add cross-surface mode command
Co-authored-by: luxiaolu4827 <227715866+luxiaolu4827@users.noreply.github.com>
2026-07-26 21:13:03 -07:00
Frowtek
a228b81501 fix(sessions): preserve recently active sessions during pruning 2026-07-26 19:30:21 -07:00
menhguin
59482ea800 fix(skills): never change a skill's source registry on update
`hermes skills update` could silently replace a same-named skill from a
DIFFERENT registry — deleting the user's files and rewriting the lockfile's
recorded provenance. Two defects on main:

- tools/skills_hub.py: check_for_skill_updates fell back to *all* sources
  (`... or sources`) when no adapter matched the recorded source, so any
  registry with a same-named skill could satisfy the fetch and be reported
  as an update — silently reassigning provenance. Now reports the entry as
  `unavailable` instead of cross-registry fallback.
- hermes_cli/skills_hub.py: do_update called do_install with a bare, slash-
  less identifier and no source constraint, letting _resolve_short_name fuzzy-
  match a same-named skill in another registry. do_install now takes an
  optional source_id pin that ABORTS (rather than falls back) when no adapter
  matches, and do_update forwards the lockfile's recorded source as that pin.

Includes regression tests reproducing the cross-registry hijack.

Salvaged from PR #72216 onto current main; re-attributed to the human
contributor.

Co-authored-by: menhguin <menhguin@users.noreply.github.com>
2026-07-26 19:29:56 -07:00
teknium1
ebbcad26ce test(memory): cover mode-aware provider deps + force reinstall path 2026-07-26 19:29:18 -07:00
LeonSGP43
5645169c8f fix(update): refresh active memory provider deps after venv rebuild
Surgical reapply of #53505 by @LeonSGP43 onto current main (stale-base
cherry-pick conflicted in main.py):

- _refresh_active_memory_provider_dependencies() in hermes_cli/main.py,
  wired into BOTH the git-pull update path (after lazy refresh) and the
  ZIP update path — the provider's plugin.yaml bridge packages are not
  in extras or LAZY_DEPS, so the core reinstall could strip/downgrade
  them and the update flow never healed them (#53272 mem0ai).
- _install_dependencies(force=True) in memory_setup.py: hand every
  declared spec to the resolver on update so missing AND version-drifted
  packages are restored (no-op when satisfied).
- Skip guards: no provider / builtin store / memory.enabled=false.

Widened beyond the original PR for the #70636 half:
- _provider_pip_dependencies(): mode-aware expansion — Hindsight in
  local/local_embedded mode needs hindsight-all (daemon + embedder), not
  just the declared hindsight-client; setup installs it but plugin.yaml
  can't express it, so update-time healing previously missed
  hindsight-embed and the daemon stayed broken.
- Spec-aware import probing: version ranges in pip_dependencies
  (mem0ai>=2.0.10,<3) no longer break the pip-name -> import-name
  mapping.

Fixes #53272. Fixes the hindsight-embed half of #70636.
2026-07-26 19:29:18 -07:00
teknium1
0fa5e41c86 feat(diff): cross-surface /diff with staged/all/session modes
Widen the cherry-picked /diff base (#4839 by @SHL0MS) into one
cross-surface implementation, folding in the review feedback and the
best ideas from the two sibling PRs (#22703, #53527):

- tools/working_diff.py: shared git collection layer — unstaged
  (default), staged, and all (vs HEAD) modes; untracked files folded in
  via `git diff --no-index` so new files appear as additions (Codex
  /diff parity); shlex-split arguments preserve quoted paths.
- CLI: handler moved to hermes_cli/cli_commands_mixin.py per the
  current god-file decomposition (dispatch stays in cli.py), renders
  through the rich console with a 400-line terminal-flood guard.
- Gateway: _handle_diff_command in gateway/slash_commands.py + dispatch
  in gateway/run.py; fenced ```diff output truncated to 60 lines /
  3000 chars before the platform senders apply their own per-platform
  message clamps (tool-progress-style layered truncation). Localized
  strings in all 17 locale catalogs.
- /diff session (from #53527): cumulative checkpoint-baseline diff of
  everything Hermes changed, via new CheckpointManager.session_diff();
  docstring records the retained-baseline approximation caveat from
  review. Works on both surfaces; degrades with an actionable message
  when checkpoints are off.
- Slack: /diff routed via /hermes diff (50-slash cap; keeps
  telegram-parity test green and /version native).
- Registry: cross-surface CommandDef with staged|all|session
  subcommands; docs: slash-commands reference (CLI + gateway tables +
  both-surfaces list) and hermes-agent skill reference.
- Tests: tests/tools/test_working_diff.py (real git repos),
  tests/hermes_cli/test_diff_command.py (real git + stubbed checkpoint
  manager), tests/gateway/test_diff_command.py (end-to-end handler,
  real checkpoint store), TestSessionDiff in
  tests/tools/test_checkpoint_manager.py.

Salvaged from the /diff PR cluster #4839 + #22703 + #53527.

Co-authored-by: Ninso112 <ninso112@proton.me>
Co-authored-by: Harshkamdar67 <harshkamdar67@gmail.com>
2026-07-26 18:28:20 -07:00
brooklyn!
e1ace0ac98
Merge pull request #72336 from NousResearch/bb/statusbar-prefs
Quieter status bar and sidebar counts
2026-07-26 20:10:16 -05:00
teknium1
07370a9dba feat(cli,gateway): unify /context into a visual context-usage breakdown
Extends the cherry-picked /context command (PR #52184) and prompt-size
attribution helpers (PR #66656) into one visual context view across
surfaces, and absorbs the per-component budget-visibility goal of the
/tokens proposal (PR #48470):

- agent/context_breakdown.py: pure renderers over the existing payload —
  a 5x20 glyph block grid (1 cell ~= 1% of the model window), an
  'Estimated usage by category' table with free space, and expanded
  per-skill / per-toolset listings via compute_context_details(), which
  reuses the prompt-size attribution mechanism (skills index-line bytes +
  registry tool->toolset map) converted to the same chars/4 heuristic.
- cli.py: /context [all] renders grid + category table (+ expanded
  listings) from the live agent and in-memory conversation history.
- gateway/slash_commands.py: /context appends the plain-text category
  table (no grid — monospace not guaranteed on messaging platforms);
  /context all adds the expanded listings. Fail-open: breakdown errors
  never break the gauge.
- hermes_cli/commands.py: /context gains the 'all' subcommand; /version
  demoted to /hermes version on Slack to keep the 50-slash cap.
- tests: renderer unit tests against synthetic payloads, registry test,
  gateway /context + /context all + failure-degradation handler tests.
- docs: slash-commands reference + CLI guide entries.

Read-only and locally computed: no provider calls, no prompt-cache impact.

Co-authored-by: RemyFevry <29257684+RemyFevry@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
2026-07-26 18:06:21 -07:00
joelbrilliant
8b3da145f1 fix(prompt-size): include names-only skills in breakdown 2026-07-26 18:06:21 -07:00
joelbrilliant
8b9423444e feat(prompt-size): per-skill and per-toolset token-cost breakdown
`hermes prompt-size` reported skills as one <available_skills> block total
and tools as one json-bytes total, so there was no way to see which
installed skill or toolset actually dominates the fixed prompt budget.

Add two additive breakdowns to compute_prompt_breakdown (hermes_cli/
prompt_size.py):

- toolsets_breakdown: each resolved tool is attributed to its single
  canonical registry toolset (registry.get_tool_to_toolset_map), summed by
  group. Fully attributable — the grand total equals the existing
  tools.json_bytes minus JSON array framing (2*count bytes).
- skills_breakdown: parsed from the rendered <available_skills> block, one
  entry per skill with two honest, distinct numbers — index_line_bytes (the
  always-on cost of listing the skill) and skill_md_bytes (on-disk SKILL.md
  size, the real read cost paid only on skill_view). Sorted largest-first
  by read cost.

render_breakdown prints both as sorted "Toolsets by size" / "Skills by
size" tables (skills capped at 20; --json carries them all). All existing
keys and output are unchanged.

Runs fully offline (dummy credentials, no network). Tests cover shapes,
largest-first ordering, per-tool attribution reconciling to the total,
namespaced-name parsing, and unmapped-skill handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:06:21 -07:00
teknium1
85c2976e22 fix(update): migrate legacy pythonw Windows gateway launchers to the hidden-console design
Two halves close the 'legacy pythonw gateways survive updates forever' gap:

1. hermes update now regenerates the installed Scheduled Task / Startup
   launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume
   phase. They are persistence artifacts written once at install time;
   updates never touched them, so pre-aa2ae36c3f installs kept launching
   the gateway through pythonw.exe forever — every descendant spawn
   flashed a conhost (#54220/#56747) and, since #70344, the console-less
   gateway died at startup with RuntimeError: sys.stderr is None (#71671).
   The task /TR points at a stable script path, so rewriting the files
   retargets it with no schtasks call and no UAC. No-op for modern
   installs; best-effort so a failed refresh never fails the update.

2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter
   to its sibling console python.exe when it exists, so the update
   pause/resume argv-replay path (and any other caller handed a legacy
   command line) respawns on the current design instead of faithfully
   resurrecting the old one. Keeps pythonw when no sibling exists — a
   failed respawn is worse than a console-less gateway.
2026-07-26 17:50:54 -07:00
teknium1
db90e36202 feat(approvals): hermes approvals suggest — mine approval history into allowlist proposals 2026-07-26 17:49:03 -07:00
teknium1
24c3c27ba8 feat(cli): hermes import-agent — import Claude Code and Codex CLI setups
Maps CLAUDE.md/AGENTS.md, permission allowlists, MCP servers, skills, and memories into their Hermes equivalents. Follows the openclaw migration pattern. Inspired by ChatGPT Work import-from-another-agent onboarding.
2026-07-26 17:47:07 -07:00
teknium1
95b7ea5e5d feat(cli): /init — generate or update AGENTS.md from a project scan
Cross-surface slash command (CLI, gateway, TUI) following the /learn prompt-injection pattern. Port of Codex /init.
2026-07-26 17:46:29 -07:00
Brooklyn Nicholson
0f7492f43a refactor(sidebar): drop session counts and the COUNT(*) that fed them
The sidebar labelled sections and workspace lanes `loaded/total`, which
read as a progress bar people expected to fill up rather than a count of
loaded rows. Pricing that label cost a COUNT(*) per profile database on
every sidebar refresh, purely so the numerator and denominator could
differ.

Pagination only needs to know whether another page exists, and that comes
free from the rows the query already returned: a window that comes back
full means more remain on disk. Sections now show the loaded count alone,
and the backend reports per-profile `profiles_truncated` flags in place of
`total` / `profile_totals`.
2026-07-26 19:36:08 -05:00
izumi0uu
35a9b3a7c2 fix(docker): ship a WAL-reset-safe SQLite runtime
Compile and checksum-pin SQLite 3.53.4 in the published image, preserve Hermes' required SQLite features, and assert the final Python linkage plus FTS5 trigram behavior during image builds.\n\nMake doctor remediation install-aware so Docker users pull and recreate every Hermes container instead of running the inapplicable git updater.\n\nFixes #70480
2026-07-26 17:20:31 -07:00
teknium1
c476ad35ae fix(update): refresh stale managed-uv catalog so SQLite runtime repair can succeed
The managed uv is installed with UV_UNMANAGED_INSTALL, which disables
'uv self update' by design — the swallowed failure left its embedded
python-build-standalone catalog frozen at bootstrap age forever.
python-build-standalone re-releases existing patch versions with fixed
SQLite (3.11.15 was re-cut with 3.53.1), so a stale catalog resolves
the same version number to the OLD vulnerable build, the probe rejects
it, and the patch-retry loop cannot recover because the fixed build
carries no newer number to try. Result: 'hermes update' printed a
guaranteed-failure provisioning warning on every run (issue #72093).

- When provisioning fails, re-bootstrap the Hermes-managed uv binary
  via the official installer (the only supported refresh for unmanaged
  installs) and retry provisioning once — only when the binary version
  actually changed, so no wasted download cycles.
- Never touch a caller-supplied uv outside the managed path.
- Soften the failure report from alarming ⚠ to informational ℹ and say
  why it is safe to wait: the WAL gate keeps databases out of WAL on
  vulnerable builds, and the next update retries.

Verified: 56 unit tests green; sabotage run (retry block removed) fails
the 3 new retry tests; live E2E replaced a fake managed uv via the real
astral installer and the refreshed binary resolved the 3.11 catalog.

Fixes #72093
2026-07-26 17:20:27 -07:00
Kevin Haddock
a75ec9278c fix(model): track explicit models: declarations in section 3 so a singular default_model doesn't suppress live discovery
A providers: entry with only a default_model/model (no explicit models:
list) is un-narrowed — the singular field is just the active selection.
Section 3 derived has_explicit_models from the merged models list, so
the lone default_model entry counted as an explicit catalog and
suppressed the /v1/models probe for no-key endpoints, leaving a
one-line /model picker menu for local llama.cpp/Ollama/vLLM servers.

Track explicit models: declarations separately at group-build time
(mirrors section 4's declaration-tracking from #40542 / PR #61928) and
gate the probe on that instead.

Salvaged from PR #68984 by @vigilancetech-com (the probe_custom_providers
gate removal in that PR is not taken — the GUI no-probe gate is
intentional).
2026-07-26 17:17:03 -07:00
HexLab98
aff48958d3 fix(deepseek): drop retired models from picker and provider defaults
Stop offering deepseek-chat/reasoner in the static catalog and point
fallback/aux defaults at the permanent v4 IDs. Keep retired aliases in
a detection-only map so /model deepseek-chat still resolves to deepseek.
2026-07-26 16:28:41 -07:00
HexLab98
cc7c418b33 fix(deepseek): remap retired chat/reasoner aliases to v4-flash
DeepSeek cut off deepseek-chat and deepseek-reasoner on 2026-07-24.
Sending those IDs now returns HTTP 400; rewrite them (and fuzzy
reasoner names) to deepseek-v4-flash so saved configs keep working.
2026-07-26 16:28:41 -07:00
trymhaak
148497f6d6 fix(kanban): strip stale session routing from dispatched worker env
A long-lived gateway can have platform routing (HERMES_SESSION_* /
HERMES_CRON_AUTO_DELIVER_*) mirrored in os.environ from a previous turn.
_default_spawn() copied that process environment verbatim into detached
kanban workers, so a worker calling kanban_create treated the inherited
chat/topic as its origin and auto-subscribed the child task — the task's
terminal notification then woke an unrelated chat.

Strip every registered session-context routing key from the worker env
unconditionally (the dispatcher is detached from every conversation);
board, workspace, task, branch, profile, model, and credential
propagation are unchanged.

Salvaged from PR #69181 (both commits squashed; the PR's second commit
fixed the first's engagement-latch assumption).
2026-07-26 16:27:52 -07:00
teknium1
a0bc7d5572 fix(kanban): snap notify-sub cursor to current MAX(task_events.id) at creation
Fixes the boot-storm half of issue #29905: kanban_notify_subs.last_event_id
defaulted to 0, so a subscription created on an already-active task replayed
the task's ENTIRE terminal-event backlog on the next notifier tick. With
many stale subs (27 observed in the report) a gateway boot after downtime
burst 100+ notifications in one go.

add_notify_sub now snaps the cursor to COALESCE(MAX(task_events.id), 0) for
the task inside the same INSERT, so new subscriptions start caught up and
only receive events that occur AFTER subscribing. The gateway slash-command
and kanban-tool auto-subscribe paths run at task creation, where the
snapshot is just the 'created' event — behavior there is unchanged.

Stale fixtures that asserted the literal 0 creation cursor now assert
'cursor unchanged/unclaimed' instead, which is what they actually meant.
2026-07-26 16:14:15 -07:00
David Beyer
0b632f772a fix(gateway): zero-sub early exit for kanban notifier board polling
Salvaged from PR #63001 (reduced scope): probe each board with the new
read-only kanban_db.count_notify_subs() before the writable connect(),
so boards with zero subscriptions are never opened writable on the 5s
notifier tick (no schema migration, no WAL/-shm sidecar churn, no
checkpoints).

The PR's machine-global .notifier.lock singleton gate was deliberately
NOT salvaged: a lock-winning default-profile gateway cannot deliver a
secondary profile's subscriptions in standalone-profile deployments
(profile routing fails closed in _authorization_adapter), so the lock
could suppress delivery entirely. The probe captures the per-tick cost
win without that regression.
2026-07-26 16:14:15 -07:00
yuexiong
83dc0b9b85 fix(install): clear stale Windows cua lock 2026-07-26 16:01:54 -07:00
yuexiong
23d0dca832 fix(install): reap Windows cua installer process tree 2026-07-26 16:01:54 -07:00
embwl0x
1bdec6f065 fix(kanban): preserve telegram dm topic metadata 2026-07-26 16:01:32 -07:00
teknium1
6d1e08b2bc fix(dashboard): QA pass — log colors, nameless channels, config bool, UX gaps
Companion fixes from a full dashboard QA pass (every page dogfooded
live), on top of the cherry-picked #31863 header-slot fix:

- ChatPage: harden the header-slot effect further — useLayoutEffect and
  never write the slot while inactive, so the handoff commentary and
  ownership rule live next to the code.
- LogsPage: level classification used raw substring matching, so INFO
  lines carrying 'parse_errors=0' (or paths like errors.log) rendered
  red. New unit-tested classifier (web/src/lib/log-classify.ts) anchors
  on the hermes_logging level token with a word-boundary fallback.
- Channels API: plugin platforms (irc, ntfy, photon, teams, …) rendered
  as nameless title-cased cards ('Irc', 'Ntfy') with empty descriptions.
  Two root causes: (1) plugin discovery never ran in the dashboard
  server process, so plugin_entries() was empty; (2) Platform enum
  pseudo-members claimed plugin ids before the registry could attach
  labels. The catalog now discovers plugins explicitly and resolves
  plugin metadata first; added descriptions + docs links for bundled
  plugin platforms and the msgraph_webhook / whatsapp_cloud / relay
  enum members. Regression test sabotage-verified against the old
  enum-first ordering.
- Config schema: updates.refresh_cua_driver declared type 'bool'
  (schema vocabulary is 'boolean'), so the switch rendered as a text
  input holding 'true'.
- Page titles: '/mcp' rendered as 'Mcp' via the naive capitalize
  fallback; literal-label table now covers MCP/Files/Channels/Webhooks/
  Pairing/System (unit-tested).
- AuthWidget: skip the guaranteed-401 /api/auth/me probe in loopback
  mode — every dashboard load logged a console error for nothing.
- Model picker: with no filter, providers that actually have models
  float above the wall of '0 models' rows.
- Cron: empty state now carries an actionable Create button.
2026-07-26 15:17:30 -07:00
xd-Neji
7b662c8d72 fix(kanban): inherit notify subs for child tasks
Copy gateway notification subscriptions from parent tasks to child tasks created by create_task(..., parents=...), link_tasks(), and decompose_triage_task().

Inherited subscriptions start at the child's current event cursor, so linking an existing child does not replay pre-link task events.
2026-07-26 14:31:47 -07:00
konsisumer
486c5ffc8d fix(cli): live-probe dashboard status and share update cleanup across paths
- hermes dashboard --status now verifies each matched PID is alive AND
  bound to a listening socket before reporting it, so stale PIDs and the
  desktop app's IPC-only 'serve --port 0' backends no longer masquerade
  as running dashboards (#58578).
- The git and Windows ZIP update paths share one
  _finish_dashboard_update_cleanup(), so the ZIP fallback gets the same
  stop/restart reporting.
- _kill_stale_dashboard_processes returns a structured
  {matched, killed, failed, unrecovered} result; the explicit was-stopped
  notice fires only for processes that could NOT be auto-restarted,
  meshing with the auto-respawn from #72192.
2026-07-26 14:11:05 -07:00
teknium1
920facdc4c fix(update): never blind-reinstall cua-driver during hermes update
'Refreshing cua-driver (Computer Use)...' could hang for minutes on
Windows: when the driver's native check-update verb returned an
indeterminate result (old driver without the verb, offline, GitHub
rate-limited, or the probe timing out), install_cua_driver(upgrade=True)
fell through to the full upstream installer — a silent, output-captured
run with a 660s ceiling, plus install.ps1's 600s concurrency-lock wait
on Windows on top. Every 'hermes update' paid that cost.

Two changes:

- install_cua_driver() grows require_confirmed_update: with it set, an
  indeterminate check keeps the installed version and returns fast,
  printing the force path (hermes computer-use install --upgrade).
  'hermes update' passes it; the explicit --upgrade CLI keeps the old
  fall-through so a force refresh still works when the check can't
  answer.
- cua_driver_update_check() default timeout is now 25s on Windows
  (8s unchanged on POSIX): first-spawn of the exe under Defender /
  SmartScreen routinely exceeds 8s, and a false timeout is exactly the
  indeterminate result that used to trigger the multi-minute reinstall.
2026-07-26 13:47:35 -07:00
teknium1
bd03960071 test(update): cover supervised-unit restart and manual argv respawn
11 new tests: owning-unit restart + dedupe + failure hint (#68934),
argv capture/respawn + --no-open + failure fallback (#40449),
/proc and ps cmdline capture, --stop never restarts.
All fail without the fix; 26 pre-existing tests unchanged.
2026-07-26 13:42:47 -07:00
MaxFreedomPollard
aa636c6fca fix(config): merge duplicate kanban block so auto_subscribe_on_create default survives
DEFAULT_CONFIG declared "kanban" twice. Python keeps only the last
literal for a duplicate key, so the first kanban block was silently
dropped and its "auto_subscribe_on_create": True default never made it
into DEFAULT_CONFIG. The consumer in tools/kanban_tools.py masks the
miss with cfg_get(..., default=True), but the documented default was
absent from DEFAULT_CONFIG (so config templates / 'hermes config show'
omit it), and the duplicate key is a standing hazard: any future key
added to the first block would also vanish.

Merge auto_subscribe_on_create into the single canonical kanban block.

Adds a regression test asserting both default sets survive and a guard
against any duplicate top-level DEFAULT_CONFIG key.
2026-07-26 13:42:22 -07:00
teknium1
339d968689 fix(setup): stop asking about self-configuring platform knobs
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
Connecting Discord asked five questions when the platform needs one. The card
listed a home channel ID you need Developer Mode to copy, an allow-all-users
security toggle, a reply-threading preference, and a home channel display name
— all with working defaults, none discoverable from the form.

Drops them from the setup surfaces entirely: the dashboard/Desktop channel
cards and the `hermes setup gateway` wizard. Discord is now bot token +
allowlist. Matrix drops from 11 fields to 7, Mattermost from 6 to 3.

Suffix-matched (`*_HOME_CHANNEL*`, `*_ALLOW_ALL_USERS`, `*_REPLY_TO_MODE`,
`*_REQUIRE_MENTION`, `*_AUTO_THREAD`, `*_FREE_RESPONSE_*`, `*_PROXY`) so plugin
platforms nobody enumerated get the same treatment. Allowlists deliberately
stay — the gateway denies everyone until one is set, so that IS the decision a
new user has to make. Required credentials are never hidden.

Nothing is removed from the product. The vars still work through
`hermes config set`, .env, and config.yaml, the gateway reads them unchanged,
and dropping them from the cards hands them back to the Keys page rather than
orphaning them (Keys hides only what a Channels card owns).
2026-07-26 08:43:24 -07:00
Teknium
e289e561c7 fix(tools): /tools shows the full pre-assembly catalog; adapt tests to tiered disclosure
CI slices 2 and 7 caught three tests broken by always-defer:

- /tools (CLI show_tools + TUI gateway tools.show) now passes
  skip_tool_search_assembly=True — it's a discovery/inspection surface,
  so users verifying an MCP installed must see deferred tools, not a
  collapsed bridge row. This also fixes
  test_slash_worker_mcp_discovery (profile MCP tool visible in /tools).
- test_plugins.py::test_plugin_tools_in_definitions: 'visible' becomes
  'reachable' — direct schema OR listed in the bridge description;
  scope-exclusion assertions unchanged (not direct AND not listed).
- test_discord_tool.py dynamic-schema-rebuild test reads the
  pre-assembly list (the rebuilt schema is what tool_describe serves).

Banner/status tool counts intentionally keep the post-assembly view —
they reflect what the model actually sees.
2026-07-26 08:26:09 -07:00
teknium1
6179da5496 fix(dashboard): one gateway liveness ladder for status + channels
The sidebar strip and the Channels page could contradict each other on
the same page load — "Gateway running" next to "The gateway is not
running." /api/status and /api/messaging/platforms each open-coded their
own liveness ladder: status probed GATEWAY_HEALTH_URL and scoped its
PID/state reads to the requested profile, messaging did neither and used
the uncached raw PID probe.

Three deployments hit the split: a cross-container gateway (no local PID,
only the health probe can see it), a profile-scoped dashboard (messaging
borrowed a DIFFERENT profile's runtime state, reporting a false
"connected" that hides a real outage — #71211), and a launch-service
managed gateway with no PID file.

Adds resolve_gateway_liveness() in gateway/status.py as the single ladder
(cached PID -> HTTP health probe -> runtime-status PID with
expected_home) and routes both endpoints, /api/messaging/platforms/{id}/test,
and the kanban dispatcher-presence probe through it. Probe callables are
injectable so the existing monkeypatch seams keep working, and
GatewayLiveness.probe_error distinguishes "down" from "couldn't tell" so
the kanban warning keeps failing OPEN instead of crying wolf.

Closes #71211.
2026-07-26 08:08:38 -07:00
Gille
914059fad6 fix(sessions): retain all reconstructed sessions 2026-07-26 08:06:46 -07:00
teknium1
d2e733e636 fix(sessions): reconstruct missing sessions instead of deleting salvaged messages
Reported in Discord by @spherohero: `sessions recover --allow-partial` copied
20,817 of 20,824 message rows, then orphan cleanup deleted every one of them
because no session row was salvageable. Final output: 0 sessions, 0 messages.
The salvage worked and then threw the result away -- the exact opposite of
what --allow-partial exists to do.

_cleanup_partial_orphans() removed dependent rows whose session_id had no
matching sessions row. That is correct when a few sessions are lost; it is
catastrophic when the sessions b-tree is damaged worse than messages, which
is the common shape (sessions is small and hot, messages is large).

Reproduced exactly: 500 readable messages, unreadable sessions -> 500
copied, 500 removed, empty output.

Now _reconstruct_missing_sessions() runs FIRST, inside the same transaction,
synthesizing a minimal row per orphaned session_id (only id/source/started_at
are NOT NULL). started_at comes from the earliest surviving message.
Placeholders carry source='recovered' and an explicit title so a fabricated
session can never be mistaken for an original. Same repro now retains all
500 messages under 1 reconstructed session.

Reconstruction is reported as LOSS, not a clean recovery: session metadata
(title, model, timestamps, cost) is genuinely gone even though the
conversation text survived. loss_detected=True, partial=True,
complete=False, with a warning naming the counts.

Also fixes total_removed_or_relinked, which summed every dict value and would
now have counted retained messages as removed.

Sabotage-verified: removing the reconstruction call restores the wipe and
fails the test. 942 targeted tests green.
2026-07-25 23:45:18 -07:00
teknium1
36926af266 test(sessions): prove the connector reached the lock before asserting blocked
Closes the last false-pass window @helix4u flagged on #71779. He explicitly
said not to hold the PR for it; it is two lines, so worth doing rather than
leaving a known-soft assertion in a concurrency test.

connection_opened.wait(1.0) proved the connection had not opened, but not
that the connector thread had actually reached connect_tracked() -- an
unscheduled thread produces the same observation. The connector now sets
connect_attempted immediately before the blocking call, and the test waits
on that first, so "still blocked" means blocked at the lock rather than
not yet started.

15/15 stable at ~1.1s. Removed-lock sabotage still fails.
2026-07-25 23:05:30 -07:00
teknium1
c8aa0c7a34 fix(sessions): report damaged state_meta as loss, not absence
Second round of @helix4u review on #71779. Both findings reproduced before
fixing.

1. My previous fix turned a crash into SILENT DATA LOSS. Returning
   status="missing" for a present-but-unusable state_meta looked like a safe
   degrade, but _verify_recovered_database only escalates "failed"/"partial"
   into a warning + loss_detected. Measured on the branch: a run that dropped
   a real metadata table reported warnings=[], loss_detected=False,
   partial=False, complete=True. Strictly worse than the ValueError it
   replaced -- that at least failed loudly.

   Now "failed" when the table exists but lacks key/value, "missing" only
   when genuinely absent. The damaged case yields
   warnings=['state_meta copy status is failed'], loss_detected=True,
   partial=True, complete=False, while staying verified=True so the output
   is still installable-with-review.

2. The race test I wrote had its own scheduling race: after the guard
   released the lock, the racer could win before the main thread set the
   release event, failing on a correct implementation. Rewritten per
   helix4u's design -- copy runs in a worker parked inside the patched
   copy, a second worker attempts connect_tracked(), assert it stays blocked,
   release, assert it then opens. Deterministic and ~1.1s instead of 10s;
   12/12 stable.

Sabotage-verified. Note the third scenario only failed after adding a
unit-level test: recover_session_database short-circuits on the inspection
result when state_meta is entirely absent, so the helper's absent-branch is
unreachable end-to-end and a regression there was invisible. Both statuses
are now pinned directly.

939 targeted tests green.
2026-07-25 23:05:30 -07:00
teknium1
9657f6e343 fix(sessions): close the snapshot check/use race and guard damaged state_meta
Post-merge follow-up to #71770. Both defects were found by @helix4u in review
and reproduced against merged main before fixing.

1. Check/use race in _copy_source_bundle (my bug, from the #71770 follow-up
   commit). It called has_live_connection(), released the registry lock, and
   only then ran shutil.copy2() over the bundle. A tracked connection could
   open in that window; the copy's close() then cancels its POSIX advisory
   locks -- the exact class #71724 closed. Measured on main: a racer thread
   opened a connection mid-copy after blocking 0.000s.

   Adds sqlite_safe_read.offline_file_access(), a context manager that holds
   the connection-lifecycle lock across an entire multi-step raw access, and
   routes the bundle copy through it. Same racer now blocks 10.0s until every
   raw descriptor is closed. Any future raw read of a database file (hashing,
   moving a bundle aside) should use this rather than a bare pre-check.

2. _copy_state_meta_salvage assumed a 'key' column. A damaged state_meta can
   keep 'value' and lose 'key'; columns.index("key") then raised ValueError
   and aborted the whole partial recovery. The mirror case (key without
   value) would have copied key-only rows and reported the table complete.
   Now requires both, matching the non-partial _copy_state_meta, so an
   unusable optional table is recorded as missing/failed and --allow-partial
   still recovers sessions and messages.

Both regression tests verified by sabotage: reinstating the bare pre-check
fails the race test, removing the key/value requirement fails the other.
937 targeted tests green.
2026-07-25 23:05:30 -07:00
Hermes Agent
92549c9a6e refactor(cli): route every aux picker through one provider-inventory seam
Adds build_aux_picker_rows() + format_aux_picker_entries() to
hermes_cli/inventory.py and converts both auxiliary-task pickers to them,
so custom providers, exclusions, and picker visibility can no longer be
dropped one call site at a time.

The two salvaged commits each fixed one kwarg at these same two sites
(user/custom providers, then for_picker). Both were per-site patches, so
the next aux picker would have reintroduced the gap. The seam makes the
correct behaviour the default a new caller cannot forget:

- user `providers:` and saved `custom_providers:` entries
- `model_catalog.excluded_providers`, matching /model
- exhausted-credential-pool providers stay visible
- only the active custom endpoint is probed, so an offline saved local
  server can't hang the picker
- the virtual `moa` row is excluded (auxiliary_client unwraps it to the
  aggregator anyway, so offering it is a silently-rewritten choice)

The vision picker also gains the current-selection marker it never had.

Also threads for_picker through build_models_payload, which had no way to
express it despite list_authenticated_providers supporting it.
2026-07-25 22:47:12 -07:00
Drexuxux
9aefa4c61c fix(model-picker): show exhausted-pool providers in the aux-task and vision pickers too
credential pool has entries but is entirely rate-limited (exhausted): rate
limits are per-model for many providers (e.g. Google Gemini), so an exhausted
key for model-A may still serve model-B, and the user should stay able to
select it. That fix wired `for_picker=True` into `list_picker_providers`.

The two sibling interactive pickers reached by `hermes model` still called
`list_authenticated_providers()` WITHOUT `for_picker=True`, so they kept
hiding exhausted-pool providers:

- `_aux_select_for_task` (hermes_cli/main.py) — the auxiliary-task
  provider/model picker.
- `_configure_vision_provider_model` (hermes_cli/tools_config.py) — the vision
  provider/model picker.

Both persist a per-task config the user runs *later* (once the momentary
cooldown clears), so hiding an exhausted-pool provider here is exactly the

Tests: two regressions assert each picker requests `for_picker=True` (they
omitted it before the fix); the existing picker/exhausted-pool suite still
passes (6 total).
2026-07-25 22:47:12 -07:00
teknium1
a1c4d99953 fix(sessions): refuse to snapshot a live database during recovery
Follow-up to the partial-recovery salvage. _copy_source_bundle() raw-copied
the source state.db and its -wal/-shm sidecars with no live-connection check.

Copying a database file is an open()/close() on it, and close() cancels every
POSIX advisory lock the process holds on that file -- including a running
VACUUM's EXCLUSIVE lock (fe431651c5). hermes_state._backup_db_file already
refuses that situation; this path did not, leaving two policies for one
hazard. Verified against the merged guard: with a tracked connection open,
_backup_db_file returned None (refused) while _copy_source_bundle copied
anyway.

Recovery normally runs as its own short-lived CLI process against an
offline/quarantined file, so this should never fire in practice. It is a
consistency fix, not a live corruption path -- but it is exactly the drift
that reintroduces the bug later.

Regression test verified by sabotage: removing the guard fails it.
2026-07-25 22:14:57 -07:00
Gille
508764d384 fix(sessions): add opt-in partial database recovery 2026-07-25 22:14:57 -07:00
teknium1
fe431651c5 fix(state): make the byte-probe guard atomic, path-correct, and fail-closed
Addresses review findings on the previous commits. Three of them were real
defects I reproduced against my own head before fixing.

1. Check/use race (BLOCKING). read_header_bytes_preopen() checked
   has_live_connection() under _live_lock, released it, then did the raw
   open/read/close outside the lock; connect_tracked() opened before
   registering. A thread could pass the "nothing is live" check, another
   could open a connection and BEGIN IMMEDIATE, and the first thread's
   close() then cancelled its POSIX locks -- the exact bug this guard
   exists to prevent. Reproduced deterministically (BLOCKED -> ACQUIRED).
   _live_lock now spans all three lifecycle transitions: open+register,
   unregister+close, and check+open+read+close.

2. Read-only connections keyed by URI spelling (BLOCKING). SessionDB's
   read-only path opens file:/…/state.db?mode=ro; that string was fed to
   Path.resolve(), producing <cwd>/file:/…/state.db?mode=ro. No probe of
   the real Path could match, so read-only connections were invisible to
   the guard and their locks cancellable. Reproduced with no forced
   scheduling. Keys now come from PRAGMA database_list (canonical path),
   with an explicit tracking_path override.

3. Fail-open wrapper (HIGH). _connect_tracked_db() caught every exception
   and retried an untracked plain connect, so any error silently disabled
   the guard. Now only ImportError (scaffold installs without hermes_cli)
   falls back; real failures propagate.

4. Backup paths that warned and proceeded (MEDIUM). _backup_corrupt_db()
   and _backup_db_file() raw-read live databases; they now REFUSE when a
   connection is live rather than warning. Losing a forensic copy beats
   corrupting the database being rescued.

Custom factories are no longer rejected (that broke legitimate callers) nor
silently untracked -- the tracking close() is mixed into whatever factory is
in play, including when an opener substitutes its own after the fact.

WAL POLICY: #70055 is RESTORED, not reverted. My earlier justification was
confounded -- the clean WAL result came from 3.53.1, which carries both the
WAL-reset fix AND 3.51.0's broken-lock defenses, so it said nothing about the
bundled 3.50.4. Re-measured on 3.50.4 with the lock fix in place: WAL 0/3 and
DELETE 0/3, i.e. no evidence WAL is safer. Upstream still documents the
WAL-reset bug through 3.51.2 as serious. Keeping new databases out of WAL
until a fixed runtime ships is the conservative call, and the WAL policy does
not belong in this root-cause fix.

Six sabotage runs confirm each new test fails when its defect is reinstated
(including two that initially did NOT -- the race test was rewritten to pause
inside the byte read, and a separate test added for the opener-substituted
factory path). 1124 targeted tests green.
2026-07-25 21:44:43 -07:00
teknium1
95fb477856 fix(state): close the tracking leak and finish the audit of raw DB reads
Completeness pass over the previous commit.

Registry leak (would have silently disabled the guard): the first version
incremented on open but decremented only in SessionDB.close(). kanban's
connect() hands raw connections to callers who close them directly (4 sites),
so its counter only ever went up — after enough kanban operations every
byte-probe on that path would be refused forever, disabling zeroed-file and
header detection. Replaced manual track/untrack with a TrackedConnection
subclass that untracks in close(), the one method every close path goes
through. Verified across plain close, contextlib.closing, double close,
nested lifetimes, and 100-cycle churn; `with conn:` (a transaction scope, not
a close) correctly stays tracked.

Also: a caller-supplied factory now wins instead of raising TypeError on a
duplicate kwarg, since tracking is an optimisation for the probe guard, not a
precondition for opening the database.

Removed _apply_delete_for_wal_reset_bug, dead after the force-DELETE revert.

Audit notes:
- DELETE mode is still reachable via the NFS/SMB/FUSE fallback and remains
  correct there; those users are protected by the raw-read fix, not by the
  journal mode.
- The remaining whole-file reads are on genuinely offline artifacts:
  _backup_db_file (DB won't open; bytes preserved for forensics),
  _backup_corrupt_db (quarantine path, now warns if a connection is live),
  and backup verification of snapshots. backup._safe_copy_db already uses
  the SQLite backup API rather than a byte copy.
- The mechanism is POSIX-specific (Windows byte-range locks are handle-scoped,
  not process-scoped), so this is a Linux/macOS correctness fix; the change is
  platform-neutral and safe on Windows.

Sibling tests updated: session recovery no longer expects a DELETE-mode
recovered DB, and the kanban WAL-fallback test patches the connect site that
actually runs now.
2026-07-25 21:44:43 -07:00
teknium1
fbd5e5772b fix(state): stop cancelling our own POSIX locks on live SQLite databases
`hermes sessions optimize` could corrupt state.db. Root cause is Hermes,
not the SQLite WAL-reset bug (#69784).

close() on ANY file descriptor for a SQLite database cancels every POSIX
advisory lock the process holds on that file, including a running VACUUM's
EXCLUSIVE lock (sqlite.org/howtocorrupt.html section 2.2). Hermes byte-probed
live databases in several hot paths: the zeroed-state.db detector runs on every
SessionDB construction (and the gateway builds those constantly), and kanban's
post-commit invariant check ran after every COMMIT. While VACUUM rewrote the
file, those probes dropped its lock and let other processes write into it.

A/B against the real code, only variable being the raw read:

  SQLite 3.50.4, VACUUM + concurrent writers, DELETE mode
    raw open/close during VACUUM   8 vacuums, 319 vacuum errors, 2/2 corrupt
    no raw read (control)        229 vacuums,   0 vacuum errors, 0/2 corrupt

  SQLite 3.53.1 (WAL-reset FIXED) reproduces identically: 2/2 corrupt.
  After this change: 0/4 corrupt, 0 vacuum errors.

Because the upgraded runtime corrupts too, replacing the embedded SQLite does
not fix this class; and because DELETE is where it reproduces, #70055's
"force DELETE on vulnerable builds" mitigation steered users into the failing
mode. That gate is reverted here: vulnerable builds get WAL again and still
warn so operators can upgrade.

- add hermes_cli/sqlite_safe_read.py: read page_count via PRAGMA over the
  existing connection instead of open()+seek(28); byte-level probes are
  restricted to before any connection exists and refused once one is live,
  with an explicit force= escape for offline artifacts (snapshots, archives)
- track live connections in SessionDB and kanban's connect so that guard is
  enforced rather than merely documented
- kanban's torn-extend check now only applies under a rollback journal; in WAL
  a committed page may still legitimately sit in the -wal file
- revert the force-DELETE WAL gate and update the tests that pinned it

Regression tests assert the behavioural contract (an external process stays
locked out across Hermes' inspection calls) and were verified to fail when the
old raw-open behaviour is restored.
2026-07-25 21:44:43 -07:00
brooklyn!
06573617e5
Merge pull request #71692 from NousResearch/bb/woa-native-arch-probe
fix(update): drop the GetNativeSystemInfo probe that lies under WoA emulation
2026-07-25 21:56:05 -05:00