Commit Graph

2167 Commits

Author SHA1 Message Date
helix4u
d9829ab45f fix(model): match custom provider by active base url 2026-05-19 14:50:38 -07:00
Teknium
544c31b50b
perf(agent-loop): cut 47% of per-conversation function calls via 3 targeted hot-path optimizations (#28866)
* perf(config): add load_config_readonly() fast path for hot agent loop

`load_config()` is called from the agent loop's per-API-call hot path via
`get_provider_request_timeout()` and `get_provider_stale_timeout()` —
both invoked once per turn from `_resolved_api_call_timeout()` in
run_agent.py.

Profiling a synthetic 20-tool-call agent run revealed:
- 21 invocations of `load_config()` cumulating 56ms (~17% of agent loop)
- 34,398 deepcopy calls totaling 37ms (config defensive deepcopy + chain)
- 8,652 `_expand_env_vars` invocations (~412 per turn)

Microbench (cache-hit, real config.yaml present):
  load_config()          265us/call  (125us deepcopy + 140us infra)
  load_config_readonly() 138us/call  (~48% faster)

`load_config_readonly()` returns the cached dict directly without the
defensive deepcopy. Documented contract: caller must not mutate. Returns
plain dict (not MappingProxyType) so downstream `isinstance(x, dict)`
guards keep working — caught during initial implementation when
MappingProxyType broke get_provider_request_timeout's guard logic.

Wired into hermes_cli/timeouts.py (the two functions called per agent
turn). load_config() is unchanged for the 263 other call sites that
mutate the result before save_config(), are not in the hot path, or
where the safety guarantee matters more than the perf.

Profile A/B (cached config, 21-turn agent loop):
                                BEFORE  AFTER   delta
  get_provider_request_timeout  55ms    16ms    -71%
  total function calls          399k    160k    -60%
  deepcopy calls (in hotspots)  34,398  ~0      ~elim

Verified:
- isinstance(load_config_readonly(), dict) is True
- timeout/stale resolutions correct
- load_config() still returns isolated mutable deepcopies
- tests/hermes_cli/test_config*.py / test_timeouts.py: 102/102 pass
- tests/cli/ + tests/agent/test_auxiliary_client.py: 883/883 pass

* perf(redact): substring pre-screens skip non-matching regex chains

Every log record passes through `RedactingFormatter.format` which calls
`redact_sensitive_text`, which historically ran ALL 13 secret-pattern
regexes against every line — including DB connection strings, JWTs,
Discord mentions, Signal phone numbers, etc. — even for typical clean
log records like 'INFO run_agent: API call completed'.

Add cheap substring pre-checks before each regex pass. False positives
still run the regex (which then matches nothing); false negatives are
impossible because every pattern requires the gated substring to match
its leading anchor:

- `_PREFIX_RE`        gated on any of 33 known credential prefix substrings
- `_ENV_ASSIGN_RE`    gated on `=` in text
- `_JSON_FIELD_RE`    gated on `:` and `"` in text
- `_AUTH_HEADER_RE`   gated on `uthorization`/`UTHORIZATION` in text
- `_TELEGRAM_RE`      gated on `:` in text
- `_PRIVATE_KEY_RE`   gated on `BEGIN` and `-----`
- `_DB_CONNSTR_RE`    gated on `://` in text
- `_JWT_RE`           gated on `eyJ` in text
- URL userinfo/query  gated on `://`
- `_redact_form_body` gated on `&` and `=`
- `_DISCORD_MENTION_RE` gated on `<@`
- `_SIGNAL_PHONE_RE`  gated on `+`

Microbench (5 typical log records, 20k iterations each):
                              BEFORE  AFTER  delta
  redact_sensitive_text per call  5.63us  1.79us  -68%

Real-world impact: ~244 log records emitted in a 30-turn agent loop, so
the chain saves ~1ms of CPU per conversation. Bigger win is the
reduction in regex execution and GC pressure during heavy logging
sessions (verbose logging, gateway message processing).

Security regression test: 30 secret-containing inputs (sk-/ghp_/JWT/DB
connstr/Auth-Bearer/private key/URL userinfo/Discord/Signal/etc.)
verified to produce identical redacted output before/after. All 75
existing tests/agent/test_redact.py cases pass.

The `?access_token=foo&code=bar` (bare query string, no scheme) case
that 'leaks' is pre-existing behavior — the URL query redaction
requires a well-formed URL with scheme+host. Not a regression.

* perf(run_agent): cache _needs_thinking_reasoning_pad result per (provider, model, base_url)

Profile of a 31-turn synthetic agent run shows `_needs_thinking_reasoning_pad`
fires 495 times (~16 per turn) and each call ran 3 helper methods, each
hitting `base_url_host_matches` 1-4 times via `urlparse`. Total cost:
3,342 base_url_host_matches calls + 3,373 urlparse calls accounting for
~36ms of agent-loop overhead (~7% of the entire post-network work).

Provider / model / base_url don't change during a conversation except via
`switch_model` and fallback activation — both of which already overwrite
those attributes atomically. Cache the result on a tuple key; since the
key is derived from the very fields that would change, the cache
auto-invalidates on the next read after a switch. No manual invalidation
needed in switch_model / _try_activate_fallback.

Profile A/B (31-turn cached-config agent run):
                                      BEFORE  AFTER  delta
  _needs_thinking_reasoning_pad cum    18ms    1ms    -94%
  _copy_reasoning_content_for_api cum  17ms    1ms    -94%
  base_url_host_matches calls          3,342   372    -89%
  urlparse calls                       3,373   403    -88%
  total function calls                 296k    223k   -25%

Verified:
- tests/run_agent/test_deepseek_reasoning_content_echo.py: 36/36 pass
- tests/run_agent/ (full): 1383/1383 pass + 3 skipped
2026-05-19 14:25:10 -07:00
teknium1
6a159be7ca fix(runtime): treat 'ollama'/'vllm'/'llamacpp' aliases like 'custom' for base_url trust (#27132)
When config.yaml has provider: ollama (or vllm/llamacpp/llama-cpp) with a
non-loopback base_url, auth.py's resolve_provider() correctly normalises
the alias to 'custom' at the top level, but two sites in runtime_provider.py
were still comparing the *original* string against the literal 'custom':

  - _config_base_url_trustworthy_for_bare_custom() rejected non-loopback
    URLs because cfg_provider_norm was 'ollama', not 'custom'.
  - _resolve_openrouter_runtime() only entered the trust branch when
    requested_norm == 'custom'.

Both sites now consult resolve_provider() and treat any alias that
resolves to 'custom' identically. Result: provider: ollama + LAN IP no
longer silently falls through to OpenRouter (HTTP 401), matching the
behaviour of provider: custom with the same base_url.

E2E verified across 6 cases (ollama/vllm/llamacpp/custom + LAN; ollama +
loopback; openrouter + cloud) — all route to the configured endpoint;
'frobnicate' + LAN still rejects with AuthError as before.

Also adds scripts/release.py AUTHOR_MAP entry for @stepanov1975
(PR #22074 — wizard config picker preservation, cherry-picked into the
preceding commit).
2026-05-19 14:23:19 -07:00
stepanov1975
e13f242f01 fix(cli): preserve setup config picker writes
Resync the setup wizard's in-memory config after the shared model picker writes to disk so the wizard's final save does not overwrite auxiliary choices or other provider updates.\n\nAdds a regression test for auxiliary task choices saved by the picker.
2026-05-19 14:23:19 -07:00
Kyle Jeong
90be1be501 fix: register browse-sh in per-source limits and --source choices
- Add 'browse-sh' to _PER_SOURCE_LIMIT in both do_browse() and
  browse_skills() with limit=500 (covers full 171-skill catalog)
- Add 'browse-sh' to --source argparse choices for both
  'hermes skills browse' and 'hermes skills search'

Without these, browse-sh fell back to the default cap of 50 results
and was not filterable via --source.
2026-05-19 14:17:38 -07:00
nekwo
d948de39e9 fix(gateway): harden Windows gateway install lifecycle
Preserve Windows profile install decisions across UAC handoff, avoid visible console windows by launching via pythonw, make repeated install/start idempotent, recreate stale Scheduled Tasks, and separate start-now from login auto-start behavior. Add Windows gateway regression coverage and systemd setup tests for the shared install flow.
2026-05-19 11:23:15 -07:00
Teknium
2a7308b7c4
fix(update): quarantine hermes.exe vs concurrent Windows instance (#26670) (#26677)
* fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine

Closes #26670.

When 'hermes update' runs on Windows with another hermes.exe alive (most
commonly the Hermes Desktop Electron app's spawned backend) _quarantine_running_hermes_exe()
fails to rename the venv shim with [WinError 32]. uv pip install -e .
then exits 2, the git-pull fast path is silently abandoned, and the ZIP
fallback runs (and fails the same way) before eventually succeeding.

This change implements three of the five proposed fixes from the issue:

1. Concurrent-instance detection (preferred fix). _detect_concurrent_hermes_instances()
   uses psutil to enumerate processes whose .exe is one of our venv shims
   (hermes.exe / hermes-gateway.exe), excluding the caller's PID. When any
   match exists, cmd_update prints an actionable message naming the
   blocking PIDs and exits 2 BEFORE any destructive work. New --force flag
   bypasses the gate.

2. Retry + restart-deferred fallback. _quarantine_running_hermes_exe()
   now retries the rename up to 4 times with 100/250/500/1000 ms backoff
   (covers the transient AV-scanner-handle case). If all retries fail,
   it schedules the replacement via MoveFileExW with the OS deferred-rename
   flag so the new shim can land at the original path and the update
   completes; the old image is fully unloaded after the user's next
   system restart.

3. Actionable warning text. The old 'Could not quarantine: [WinError 32]'
   warning is replaced with one that names the likely culprits (Hermes
   Desktop, REPLs, gateway, AV) and points to the new --force flag.

Tests:
- 13 new tests in tests/hermes_cli/test_update_concurrent_quarantine.py
  covering: psutil-based enumeration, self-pid exclusion, case-insensitive
  matching of .EXE, no-psutil graceful degradation, off-Windows no-op,
  helpful warning formatting, retry-then-succeed, restart-deferred fallback,
  cmd_update abort + exit code 2, and --force bypass.
- New autouse fixture in tests/hermes_cli/conftest.py defaults
  _detect_concurrent_hermes_instances to [] so the rest of the suite
  isn't tripped by the developer's own running hermes.exe. Opt-out marker
  'real_concurrent_gate' registered in pyproject.toml.
- Updating docs page (website/docs/getting-started/updating.md) gains a
  short section explaining the new Windows error and remediation.

* chore: refresh uv.lock to match pyproject.toml exact pins

aiohttp 3.13.4 -> 3.13.3 (matches pyproject pin: aiohttp==3.13.3)
anthropic 0.87.0 -> 0.86.0 (matches pyproject pin: anthropic==0.86.0)
hermes-agent 0.13.0 -> 0.14.0 (matches pyproject version)

CI's uv lock --check was failing on the merged state because main
drifted: pyproject.toml uses exact == pins for those two deps and the
hermes-agent version was bumped to 0.14.0 but the lockfile still had
0.13.0.
2026-05-19 11:10:51 -07:00
LeonSGP43
ebe0b77122 fix(model-switch): mark bare custom provider as current 2026-05-19 10:57:35 -07:00
kshitijk4poor
7552e0f3c0 fix(kanban): also hoist idx_events_run + drop redundant inner create
Extends the previous commit to cover the remaining additive-column index
that sits on the same migration trap:

- ``task_events.run_id`` -> ``idx_events_run`` was still in SCHEMA_SQL.
  A legacy ``task_events`` table predating #17805 (no ``run_id``) would
  still abort ``executescript`` before ``_migrate_add_optional_columns``
  could add the column. Hoisted out of SCHEMA_SQL and made unconditional
  in the migration alongside the other three indexes.

- Removed the now-redundant ``CREATE INDEX idx_tasks_idempotency`` that
  was nested inside the ``if "idempotency_key" not in cols`` branch.
  The unconditional create lower in the function makes it idempotent
  on both fresh and legacy DBs.

- Strengthened the regression test to cover all four indexes
  (``idx_tasks_session_id``, ``idx_tasks_tenant``, ``idx_tasks_idempotency``,
  ``idx_events_run``) and to seed a pre-#17805 ``task_events`` shape that
  exercises the ``run_id`` migration path.

The result: every ``CREATE INDEX`` that depends on an additive column now
runs after the migration ensures the column exists. Verified against a
realistic pre-#16081 board fixture (tasks + task_events both legacy
shape) — origin/main reproduces ``no such column: session_id``; this
branch migrates cleanly and creates all four indexes.
2026-05-19 08:09:11 -07:00
Michael Nguyen
7c622b6c74 fix(kanban): migrate task session index after columns 2026-05-19 08:09:11 -07:00
Teknium
7bcdced6c1
fix(kanban): respawn guard defers blocker_auth instead of auto-blocking (#28683)
Follow-up to #28455. The respawn guard's blocker_auth rule (last error
matched a quota/auth/429 pattern) was auto-blocking the task on first
occurrence. That's too aggressive: transient rate limits typically
clear in seconds to minutes, but the auto-block puts the task in
'blocked' status which requires manual unblock.

Now treats blocker_auth the same as recent_success and active_pr:
defer the spawn this tick, leave the task in 'ready', let the next
tick try again. If the auth error genuinely persists, the existing
consecutive_failures counter trips the auto-block circuit breaker
after failure_limit failures via the normal path — so a persistent
401/403/quota-exhausted still ends up blocked, just not on first hit.

Also documents the respawn_guarded event in kanban.md's events table
with the three guard reasons.

Updated test_dispatch_respawn_guard_auto_blocks_auth_error → renamed
to test_dispatch_respawn_guard_defers_auth_error_without_auto_block;
asserts task stays in 'ready' and the guard reason is recorded.
2026-05-19 03:27:45 -07:00
Teknium
88ee58f7d2
fix(kanban): stale reclaim must not tick failure counter (#28680)
Follow-up to #28452. detect_stale_running() was calling
_record_task_failure() on every reclaim, which ticked the
consecutive_failures counter. With the default failure_limit=2,
two legitimately long-running tasks (>4 h without explicit
heartbeat) would auto-block via the spawn-failure circuit
breaker — even though no worker actually failed.

Stale reclaim is dispatcher-side absence-of-heartbeat detection,
not a worker fault. Removed the _record_task_failure() call;
the 'stale' event in task_events is still the audit surface,
but the failure counter is now reserved for spawn_failed /
timed_out / crashed (real failures).

Also documents the heartbeat requirement:
- KANBAN_GUIDANCE in agent/prompt_builder.py now states the
  rule ('call kanban_heartbeat at least once an hour for tasks
  running longer than 1 hour') so workers learn the contract.
- kanban.md adds the stale event row to the events table and
  flags the heartbeat requirement in the worker lifecycle list.

New regression test: test_detect_stale_does_not_tick_failure_counter
locks in the new behaviour.
2026-05-19 03:15:18 -07:00
Teknium
aedb8ac83b
feat(update): syntax-validate critical files post-pull, auto-rollback on failure (#28669)
Catch the PR #28452 failure mode (orphan merge-conflict markers in
hermes_cli/config.py) on the user side: after git pull succeeds, compile
the files every 'hermes' invocation imports at startup. If any has a
syntax error, git reset --hard back to the pre-pull SHA so the install
stays bootable. User can retry once a fix lands upstream.

- New _capture_head_sha() + _validate_critical_files_syntax() helpers
- Wires both into _cmd_update_impl after the pull/reset succeeds
- Tests cover the helpers, the rollback flow, and a production-tree
  invariant (CI fails if main itself has a syntax error in a critical
  file — catches future broken commits before users hit them)
2026-05-19 03:01:02 -07:00
xxxigm
12c39830f0 fix(doctor): attach codex CLI hint to OpenAI Codex auth warning for #27975
`hermes doctor` printed 'codex CLI not installed (optional — ...)' as a
generic info line at the bottom of the auth section, several rows below
'OpenAI Codex auth (not logged in)' and after MiniMax/Gemini auth checks.
Users reading sequentially mistook it for MiniMax-related advice.

Move the hint up under the Codex auth warning so it's adjacent to the
row it actually pertains to. Behavior unchanged when the codex CLI is
installed (success path keeps its 'codex CLI ✓' row at the bottom).
Tests cover both placement and suppression cases.

Salvage of @xxxigm's 3-commit stack (#27986).
Closes #27975.
2026-05-19 00:14:39 -07:00
vanthinh6886
62573f44cf fix: guard yaml.safe_load, flock unlock, TOCTOU races, and atomic writes
1. trajectory_compressor.py: yaml.safe_load() returns None on empty
   files, crashing with TypeError on `if 'tokenizer' in data`. Fix by
   adding `or {}` fallback. (HIGH — blocks startup with empty config)

2. 6 files with fcntl.flock(LOCK_UN) in finally blocks without
   try/except: cron/scheduler.py, hermes_cli/auth.py,
   agent/shell_hooks.py, tools/skill_usage.py,
   tools/environments/file_sync.py, tools/memory_tool.py. If unlock
   raises OSError, fd.close() is skipped and the lock is held forever.
   The msvcrt branches already had try/except; the fcntl branches did
   not. Fix by wrapping in try/except (OSError, IOError): pass.

3. agent/copilot_acp_client.py line 639: TOCTOU race — path.exists()
   followed by path.read_text() with no try/except. If file is deleted
   between the check and the read, FileNotFoundError propagates. Fix
   by using try/except FileNotFoundError.

4. gateway/sticker_cache.py: non-atomic write via Path.write_text()
   can leave truncated JSON on crash, causing JSONDecodeError on next
   load. Fix by writing to tempfile + fsync + os.replace (atomic).
2026-05-19 00:12:41 -07:00
noctilust
425aba766b fix(cli): ignore stale HERMES_TUI_RESUME env
HERMES_TUI_RESUME is an internal env var the Python wrapper exports to hand
a session ID off to the Ink TUI. Because _launch_tui started from
os.environ.copy(), any exported/stale value in the user's shell leaked
through — so plain `hermes --tui` would try to resume a missing session
and leave the UI at 'error: session not found' with no live session.

Drop HERMES_TUI_RESUME from the env before conditionally re-setting it
from the argparse-resolved resume_session_id. Tests cover both the drop
path and the set-from-arg path.

Salvage of #28080 by @noctilust.
2026-05-19 00:10:15 -07:00
YuanHanzhong
afffb8d9a5 fix(dashboard): use browser scrollback for chat wheel 2026-05-19 00:07:33 -07:00
Teknium
22120ef00f Revert "feat(telegram): support quick-command-only menus"
This reverts commit b1acf80e17.
2026-05-18 23:59:57 -07:00
stevehq26-bot
b1acf80e17 feat(telegram): support quick-command-only menus 2026-05-18 22:48:42 -07:00
Martin
417a653d9e fix(gateway): prevent Windows Telegram /restart leaving gateway stopped 2026-05-18 22:38:08 -07:00
CryptoByz
f260aa6dc0 fix(telegram): recover from post-update polling conflict without entering limbo 2026-05-18 22:01:31 -07:00
Jpalmer95
dfcf48b476 feat(kanban): drag-to-delete trash zone + bulk delete for task cards
Salvages #28125 by @Jpalmer95. Adds:
- Drag-to-delete trash zone in the kanban dashboard
- Bulk delete endpoint with cascading delete_task cleanup
- Frontend updates (drag visual + drop handler)
- Confirmation prompt before delete

Resolved end-of-file test conflict by appending both halves.
2026-05-18 21:40:13 -07:00
roycepersonalassistant
e3823657d6 feat(kanban): add scheduled status for delayed follow-ups
Salvages #24533 by @roycepersonalassistant. Adds a first-class
'scheduled' Kanban status for time-delay follow-ups that aren't
waiting on human input.

- hermes kanban schedule <task_id> [reason] CLI command
- Dashboard/API transitions to/from Scheduled
- unblock_task() now releases both 'blocked' AND 'scheduled' tasks
  (re-checking parent dependencies before moving to ready/todo)
- i18n + docs updates

Resolved conflicts: kept HEAD's failure-counter reset on unblock
alongside the PR's scheduled state, kept HEAD's 'running' direct-set
rejection, combined both bulk-status branches. Dropped the dist/
bundle changes (months-stale; would need rebuild from source).
2026-05-18 21:39:03 -07:00
Teknium
b5c1fe78aa
feat(skills): add skill bundles — alias /<name> loads multiple skills (#28373)
Skill bundles are tiny YAML files in ~/.hermes/skill-bundles/ that
group several skills under one slash command. Invoking /<bundle-name>
from any surface (CLI, TUI, dashboard, any gateway platform) loads
every referenced skill into a single combined user message.

Use cases:
- /backend-dev → loads github-code-review + test-driven-development
  + github-pr-workflow as one bundle.
- /research → loads several research skills together.
- Team task profiles shared via dotfiles.

Behavior:
- Bundles take precedence over individual skills when slugs collide.
- Missing skills are skipped with a note, not fatal.
- No system-prompt mutation — bundles generate a fresh user message
  at invocation time, the same way /<skill> does. Prompt cache stays
  intact.
- Works in CLI dispatch, gateway dispatch, autocomplete (CLI + TUI),
  /help display.

Schema (~/.hermes/skill-bundles/<slug>.yaml):
    name: backend-dev
    description: Backend feature work.
    skills:
      - github-code-review
      - test-driven-development
    instruction: |
      Optional extra guidance prepended to the loaded skills.

New module: agent/skill_bundles.py — load, scan, resolve, build
invocation message, save, delete. yaml.safe_load only; broken
bundles log a warning and are skipped, never raise.

New CLI subcommand: hermes bundles {list,show,create,delete,reload}.
Implementation in hermes_cli/bundles.py; wired in hermes_cli/main.py.
'bundles' added to _BUILTIN_SUBCOMMANDS so plugin discovery skips it.

New in-session slash command: /bundles lists installed bundles in
both CLI and gateway. /<bundle-name> dispatch added to CLI (cli.py)
and gateway (gateway/run.py) before the existing /<skill-name> path.

Autocomplete: SlashCommandCompleter gained an optional
skill_bundles_provider parameter that defaults to None — the prompt
shows '▣ <description> (N skills)' for bundles vs '' for skills.

Tests:
- tests/agent/test_skill_bundles.py — 33 tests covering slugify,
  scan/cache freshness, resolve (including underscore→hyphen
  Telegram alias), build_bundle_invocation_message (loading, missing
  skills, user/bundle instruction injection, dedup), save/delete,
  reload diff, list sort.
- tests/hermes_cli/test_bundles.py — 8 tests for the CLI
  subcommand (create/list/show/delete/reload, --force, missing
  bundle errors).
- tests/gateway/test_bundles_command.py — 4 tests for the gateway
  handler and bundle resolution priority.

Live E2E: verified subprocess invocations of hermes bundles
{list,create,show,reload,delete} round-trip correctly against an
isolated HERMES_HOME.

Docs:
- website/docs/user-guide/features/skills.md — new 'Skill Bundles'
  section with quick example, YAML schema, management commands,
  behavior notes.
- website/docs/reference/cli-commands.md — 'hermes bundles' added to
  the top-level command table and given its own subcommand section.
2026-05-18 21:38:05 -07:00
aqilaziz
1733cb3a13 feat(kanban): configure worktree paths and branches
Salvages #26496 by @aqilaziz. Adds branch_name column + CLI flag so
tasks with workspace_kind='worktree' can pin a target branch on
create. Schema migration added to _migrate_add_optional_columns.

- Task.branch_name field + DB column + migration
- create_task accepts branch_name kwarg
- hermes kanban create --branch <name> flag
- kanban show output includes 'Branch: <name>' when set

Cherry-picked the substantive commit (a7558cf27); the PR's tip was
an unrelated service-path-dirs commit. Resolved 2 INSERT-column-list
and show-output conflicts alongside main's session_id and
max_runtime_seconds additions; kept all three.
2026-05-18 21:33:08 -07:00
Teknium
53cf82a1ea
fix(kanban): remove orphan conflict markers from kanban.py (#28459)
PR #28454 (salvage of #26745, workflow filter) merged with leftover
git conflict markers in hermes_cli/kanban.py at three sites:
- _task_to_dict() (session_id alongside workflow_template_id/current_step_key)
- p_list parser (--sort alongside --workflow-template-id/--step-key)
- _cmd_list (order_by alongside the new filter kwargs)

Cleans up the markers and keeps both halves at each site.

Resolves a self-introduced regression.
2026-05-18 21:29:31 -07:00
Teknium
1a883b421f
fix(kanban): remove orphan conflict markers from config.py (#28458)
PR #28452 (salvage of #23790, stale detection) merged with leftover
git conflict markers in hermes_cli/config.py around the
`dispatch_stale_timeout_seconds` config block, breaking config import
and any code path that loads it. Cleans up the markers and keeps both
config blocks (worker log rotation/orchestrator + stale detection).

Resolves a self-introduced regression.
2026-05-18 21:27:58 -07:00
SerenityTn
1a5172742e feat(kanban): show dashboard cron jobs across profiles
Salvages #27568 by @SerenityTn. Dashboard cron page now lists cron
jobs from all profiles, with profile-aware filter UI and storage
routing. Includes test coverage for cross-profile listing, mutation,
deletion, and validation.

Also fixes orphan conflict markers in config.py left by an earlier
salvage merge (kanban.dispatch_stale_timeout_seconds was double-nested
in HEAD/PR markers from #28452 salvage of #23790).
2026-05-18 21:26:45 -07:00
fardoche6
264e85b3dd feat(kanban): add respawn guard to block repeat worker storms
Salvages #27484 by @fardoche6. Adds a respawn guard that skips worker
spawn for tasks where:
- a recent run already succeeded (recent_success — within guard window)
- the previous run hit a quota/auth error (blocker_auth, also auto-blocks)
- a recent task comment includes a GitHub PR URL (active_pr)

The guard prevents repeat worker storms on the same bug/task. Includes
the contributor's review-findings fixup (regex hardening, observability,
auth coverage).

Resolved a small DispatchResult conflict alongside main's 'stale' field;
kept both. Authorship preserved via rebase merge.
2026-05-18 21:24:19 -07:00
nehaaprasaad
341912c224 feat(kanban): filter tasks by workflow fields and runs by status/outcome
Salvages #26745 by @nehaaprasaad. Exposes filtering for the existing
workflow_template_id and current_step_key columns:

- list_tasks() accepts workflow_template_id and current_step_key kwargs
- 'hermes kanban list' adds matching CLI flags
- dashboard plugin_api also exposes the filters

Resolved a small conflict in list_tasks signature alongside main's
session_id and order_by additions; combined all three into the single
filter list.
2026-05-18 21:22:32 -07:00
thewillhuang
e286e68756 feat(kanban): stale detection for running tasks in dispatcher
Salvages #23790 by @thewillhuang. Adds detect_stale_running() to
the dispatcher cycle. Running tasks that have been started for longer
than dispatch_stale_timeout_seconds (default 14400 = 4h) without a
heartbeat in the last hour are auto-reclaimed to ready.

- New config kanban.dispatch_stale_timeout_seconds (default 14400, 0 disables)
- New 'stale' field on DispatchResult
- detect_stale_running() in kanban_db.py with heartbeat freshness check
- Records outcome='stale' on run close + 'stale' event; ticks failure counter
- Wires config through gateway embedded dispatcher
- Updates _cmd_dispatch verbose/JSON output and daemon logging

Resolved test-file end-of-file conflict by appending both halves.
2026-05-18 21:20:56 -07:00
thewillhuang
f55d94a1e0 feat(kanban): wire dispatcher to dispatch review agents from review column
Salvages #23772 by @thewillhuang. Adds 'review' as a valid kanban task
status and extends dispatch_once to monitor the review column as a
second dispatch source (in addition to the existing ready column).

- Adds 'review' to VALID_STATUSES
- Adds claim_review_task() — atomically transitions review → running
- Adds has_spawnable_review() — health telemetry mirror
- Extends dispatch_once with a review column dispatch loop
- Review agents get 'sdlc-review' skill auto-loaded

Resolved 2 conflicts (VALID_STATUSES merge with main's 'scheduled' state,
test file additions). Adapted claim_review_task to main's
ttl_seconds: Optional[int] = None convention (matches claim_task).
2026-05-18 21:19:51 -07:00
awizemann
31fe229039 feat(kanban): stamp originating ACP session_id on tasks
Salvages #23208 by @awizemann. Tracks which chat session created a
kanban task so clients can render a per-session board without falling
back to tenant + time-window heuristics.

- Schema: tasks gains nullable session_id TEXT column with index
  (additive migration in _migrate_add_optional_columns).
- ACP: server.py exposes the originating session id via HERMES_SESSION_ID
  with save/restore around the agent loop.
- Tool: kanban_create reads HERMES_SESSION_ID (with explicit override).
- CLI: 'hermes kanban list --session <id>' filter; JSON output exposes
  session_id.
2026-05-18 21:15:21 -07:00
Niraven
3ee7a5546d feat(cli): add kanban swarm topology helper
Salvages #26791 by @Niraven. Adds 'hermes kanban swarm' to create a
durable Kanban Swarm v1 graph: a completed root/blackboard card,
parallel worker cards, a verifier gated on all workers, and a
synthesizer gated on the verifier. Stores shared swarm blackboard
updates as structured JSON comments on the root card.

Self-contained: new hermes_cli/kanban_swarm.py module + CLI wiring +
unit tests.
2026-05-18 21:10:12 -07:00
loicnico96
79f6654d16 feat(kanban): surface per-task model_override in show + tool output
Salvages #26897 by @loicnico96. The per-task model_override DB column
already exists on main, but it wasn't exposed in user-facing surfaces.
This adds:
- 'kanban show' prints 'model: <name>' when model_override is set
- kanban_show / kanban_list tool responses include the model_override field

Original branch was stale (PR was authored against an older field name
'model'); applied the substantive surface exposure manually using the
current 'model_override' field name.
2026-05-18 21:09:02 -07:00
LizerAIDev
a846e500b0 feat(kanban): add --sort option to 'hermes kanban list'
Salvages #25745 by @LizerAIDev. Adds --sort {created,created-desc,
priority,priority-desc,status,assignee,title,updated} to 'hermes kanban
list'. Validated against VALID_SORT_ORDERS map; invalid values raise
ValueError. Default behaviour (priority DESC, created ASC) is unchanged
when --sort is omitted.
2026-05-18 20:58:43 -07:00
Bartok9
365da2d2df fix: 4 small surgical bugs
Salvages #23302 by @Bartok9. Four independent one-area fixes:

1. kanban boards delete alias now hard-deletes (not archives) — the
   alias didn't carry --delete, so getattr(args, 'delete', False)
   returned False. Detect boards_action=='delete' explicitly.
2. Gateway auto-title failures no longer leak as user-visible
   warnings — debug-log only since they're not actionable.
3. Background process completion notification snaps truncation to
   the next newline boundary, prepends a marker when content is
   dropped.
4. _cprint() schedules the run_in_terminal coroutine via
   asyncio.ensure_future so output isn't silently dropped from
   background threads (fixes #23185 Bug A). Skips the
   double-print fallback that would fire for mock paths.
2026-05-18 20:54:52 -07:00
SimbaKingjoe
5fdcfd851f feat(kanban): add max_in_progress config to cap concurrent running tasks
Salvages #22981 by @SimbaKingjoe. Adds 'kanban.max_in_progress' config
that caps simultaneously running tasks. When the board already has N
running, dispatcher skips spawning so slow workers (local LLMs,
resource-constrained hosts) don't pile up and time out.

Threads through dispatch_once(max_in_progress=) and gateway dispatcher
config parsing with validation (warns on invalid/below-1 values).
2026-05-18 20:50:13 -07:00
LeonSGP43
a94ddd8073 fix(kanban): honor severity thresholds in diagnostics
Salvages #26431 by @LeonSGP43. Dashboard plugin_api list_diagnostics
was using exact-match (severity == filter), so '--severity warning'
hid 'error' and 'critical' diagnostics. Adds severity_at_or_above()
helper to kanban_diagnostics and uses it in the dashboard endpoint
(CLI already used SEVERITY_ORDER comparison correctly).
2026-05-18 20:47:01 -07:00
LeonJS
9f008bcd5c fix(kanban): release scratch workspace and tmux session on task completion
Salvages #27369 by @LeonJS. complete_task() now calls _cleanup_workspace()
and _cleanup_worker_tmux() after marking a task complete.

Scratch workspaces (used by swarm agents) accumulate on disk — hundreds
of MB per task, never released. Stale tmux sessions from completed
agents also persist indefinitely.

Both gates are safe:
- workspace_kind == 'scratch' gate preserves user worktree/dir workspaces
- tmux #{pane_dead} == 1 gate only kills sessions where the worker has
  already exited
- best-effort: cleanup failures never block task completion
2026-05-18 20:45:29 -07:00
shunsuke-hikiyama
fb96208892 feat(kanban): add initial-status for human-ops cards
Salvages #27526 by @shunsuke-hikiyama. Adds an --initial-status flag
(running|blocked, default running) to 'kanban create', threaded through
kanban_db.create_task() and the kanban_create tool schema. 'blocked'
parks the task directly in the blocked column for R3 human-ops review,
skipping the brief running-to-blocked transition.

Dropped the unrelated 'add' alias, WIFEXITED Windows compat, and
slash-handler error formatting changes that were bundled in the
original PR — those should ship as their own focused changes if still
wanted.
2026-05-18 20:44:02 -07:00
uzunkuyruk
666b66a066 fix(oneshot): pass fallback_providers from profile config to AIAgent
Salvages #23368 by @uzunkuyruk. Oneshot workers (e.g. kanban workers
spawned via 'hermes -p <profile> chat -q ...') were not honouring the
profile's fallback_providers / fallback_model chain because oneshot.py
never read the config and never passed fallback_model= to AIAgent.

Reads cfg.get('fallback_providers') (new list format) or
cfg.get('fallback_model') (legacy single-dict) with the same
normalization cli.py applies, then forwards as fallback_model=_fb.
2026-05-18 20:37:23 -07:00
oemtalks
b9d38a56dd fix(kanban): don't crash dispatched workers when kanban-worker skill is absent
Salvages #27372 by @oemtalks. The dispatcher unconditionally injected
`--skills kanban-worker` into every worker spawn, but worker profiles
sometimes don't have that bundled skill in their skills dir, which is
fatal at CLI startup (`ValueError: Unknown skill(s): kanban-worker`).

Adds `_kanban_worker_skill_available(hermes_home)` and only injects the
flag when the skill resolves. The MANDATORY lifecycle still ships via
KANBAN_GUIDANCE in the system prompt, so omitting the flag is safe.
2026-05-18 20:32:20 -07:00
Ade5954
0392cf53b5 fix(kanban): close sqlite connection on init failure to prevent fd leak
Salvages #28301 by @Ade5954. If WAL setup, PRAGMA application, or schema
init raises after sqlite3.connect() succeeds, the new connection was
leaking. Wrap the body in try/except so the connection is closed before
the exception propagates.
2026-05-18 20:30:56 -07:00
DoGMaTiiC
4da4133d34 fix: assign single-task kanban decompositions 2026-05-18 20:26:02 -07:00
roycepersonalassistant
6c4f11c64a fix: show scheduled kanban tasks in dashboard 2026-05-18 20:25:45 -07:00
ACR27
a5c2836b07 feat(kanban): allow trimmed task comments
SS-1647 live SHIP validation: real code + tests for kanban comment --max-len.
2026-05-18 20:25:29 -07:00
hanzckernel
5d079fee17 fix: harden Kanban worker Hermes command resolution 2026-05-18 20:25:09 -07:00
ht1072
0b547aea03 fix(kanban): make legacy task migration idempotent
(cherry picked from commit 293f1c3a7241b0117669e049d9aa746c9645ac90)
2026-05-18 20:24:53 -07:00
zccyman
fe5e0bf5a3 feat(kanban): add board-level default workdir (#25430) 2026-05-18 20:24:04 -07:00
LeonSGP43
8bfb456948 fix(kanban): pass accept-hooks to worker chat subprocess 2026-05-18 20:23:47 -07:00
LeonSGP43
0f620138b0 fix(kanban): make claim ttl configurable
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-18 20:23:31 -07:00
Interstellar-code
d8ad431de8 fix(kanban): task_age() tolerates ISO-8601 timestamps
Prevents ValueError crash in dashboard get_board() when a task has
an ISO timestamp (e.g. "2026-05-10T15:00:00Z") instead of a unix epoch
int. Adds _to_epoch() helper that normalises both formats.
2026-05-18 20:18:04 -07:00
psionic73
ca8126bd53 fix(kanban): serialize DB initialization 2026-05-18 20:17:48 -07:00
soynchux
9281599b6f fix(kanban): align board_exists with board discovery rules 2026-05-18 20:17:10 -07:00
bradhallett
de9bcfc6a0 fix(kanban): fingerprint crash errors to prevent fleet-wide retry exhaustion
When a systemic failure (provider outage, auth expiry, OOM) crashes
multiple workers simultaneously, detect_crashed_workers increments
each task failure counter independently. The circuit breaker only
trips after N × failure_limit retries across the fleet.

Fingerprint crash errors by normalizing host-specific details (PIDs,
timestamps). When 3+ tasks crash with the same fingerprint in a
single detection cycle, immediately trip the circuit breaker
(failure_limit=1) instead of waiting for repeated failures.

Isolated crashes (unique fingerprints) retain their normal retry
budget. Protocol violations continue to trip immediately.

Includes regression tests for systemic and isolated crash paths.
2026-05-18 20:16:50 -07:00
bradhallett
f042931852 fix(kanban): reset failure counters on unblock_task
When a task is manually unblocked (blocked → ready/todo), the
consecutive_failures counter and last_failure_error were left intact.
The next failure would immediately re-trip the circuit breaker because
the counter was still at or above the failure limit.

Reset both fields on unblock so the task gets a fresh retry budget.

Includes a regression test that verifies counters are zeroed.
2026-05-18 20:16:32 -07:00
sprmn24
5db0d72c90 fix(kanban): use 'is not None' check for max_runtime_seconds in create_task
max_runtime_seconds=0 was being silently coerced to None due to a falsy
check (if max_runtime_seconds). Zero is a valid value that causes the
dispatcher to immediately time out a task. The adjacent max_retries
parameter already used the correct 'is not None' pattern.

Fixes the inconsistency by aligning max_runtime_seconds with max_retries.
2026-05-18 20:16:15 -07:00
bradhallett
40c1decb3b fix(kanban): promote blocked tasks when parent dependencies complete
recompute_ready only scanned 'todo' tasks for promotion, ignoring
'blocked' tasks entirely. When a task was blocked (e.g. by the circuit
breaker) and its parent dependencies later completed, the task stayed
stuck in 'blocked' forever unless manually unblocked.

Now recompute_ready also scans 'blocked' tasks. When all parents are
done/archived, the blocked task is promoted to 'ready' with failure
counters reset — equivalent to an automatic unblock.

Includes a regression test for the blocked-parent-done promotion path.
2026-05-18 20:15:55 -07:00
Que0x
bc961c13f3 fix(kanban): sync slash subcommands with live parser 2026-05-18 20:15:38 -07:00
argabor
f149e1e567 fix(cli): make kanban specify max_tokens configurable 2026-05-18 20:15:20 -07:00
Zyrixtrex
b7ea62e5d3 fix(kanban): promote dependents when a parent is archived 2026-05-18 20:15:03 -07:00
Zyrixtrex
326c15d955 fix(kanban): preserve notifier_profile for dashboard home subscriptions 2026-05-18 20:14:45 -07:00
QuenVix
afae2dd9ec fix(kanban): keep board-management commands independent from board override 2026-05-18 20:14:27 -07:00
QuenVix
8a64e1580b fix(kanban): ignore stale HERMES_KANBAN_BOARD for removed boards 2026-05-18 20:14:10 -07:00
ms-alan
97ac94fe56 fix(kanban): seed bundled skills (e.g. kanban-worker) on kanban init
Closes #23725
2026-05-18 20:13:52 -07:00
momowind
4519d2b476 fix(web): add Cache-Control: no-store to plugin static file serving
Prevents browser caching of stale dashboard plugin JS files that may
contain bugs already fixed upstream (e.g. COLUMN_LABEL undefined).
2026-05-18 20:13:35 -07:00
briandevans
d62964cdfa fix(kanban): clear _INITIALIZED_PATHS in remove_board so recycled DBs re-init schema
Archiving or deleting a board via remove_board() leaves the path's
"schema already initialized" entry in the module-level cache. A
concurrent connect(board=<slug>) call (e.g. the dashboard event-stream
poll loop) then:

  1. resolves the same kanban.db path,
  2. recreates the directory + an empty sqlite file because
     connect() does mkdir(parents=True, exist_ok=True),
  3. skips the CREATE TABLE pass because the cache entry says the
     schema is already in place,
  4. errors on the next read with `no such table: task_events`.

Drop the cache entry before mutating the filesystem so the fresh file
gets a proper schema init on next connect(). Applies to both
archive=True (rename) and archive=False (rmtree) branches.

Fixes #23833.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:13:17 -07:00
hongchen1993
f01ee0b575 feat: per-task model override for kanban workers
- Add model_override field to Task class and tasks schema
- Add migration for existing databases
- Spawn worker with -m model when model_override is set
2026-05-18 20:12:28 -07:00
xxxigm
cafbc9a734 feat(cli): wire --manual-paste into `hermes auth add and hermes model`
Register the new ``--manual-paste`` flag on both entry points and
thread it through to the xAI loopback login:

* ``hermes auth add xai-oauth --manual-paste`` — pool-add path,
  forwarded inside ``auth_commands.handle_auth_add``.
* ``hermes model --manual-paste`` — model-picker path, forwarded
  by ``_model_flow_xai_oauth`` into the synthetic ``argparse.Namespace``
  it passes to ``_login_xai_oauth``.  The picker also now forwards
  ``--no-browser`` and ``--timeout`` for consistency (previously
  hardcoded to defaults regardless of CLI flags).

Help text on both flags points at #26923 and names the
browser-only remote consoles (Cloud Shell, Codespaces, EC2
Instance Connect) so users searching ``hermes --help`` can find
the workaround.
2026-05-18 20:10:52 -07:00
xxxigm
5a5c265bcf fix(oauth): add manual-paste fallback for browser-only remote consoles
xAI Grok OAuth (and Spotify) use a loopback redirect to
``http://127.0.0.1:<port>/callback`` to capture the authorization
code. That works when the browser and Hermes run on the same
machine, and the SSH tunnel recipe handles the regular remote
case. It breaks completely on **browser-only remote consoles**
(GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect,
Gitpod, Replit, …) where the user has a browser but no real SSH
client to forward a port — the redirect to 127.0.0.1 on the
remote VM simply isn't reachable from the laptop, and there's
nothing the existing flow can do about it (#26923).

This commit adds the foundation for a manual-paste fallback:

* ``_is_remote_session`` now also recognises Cloud Shell,
  Codespaces, Gitpod, Replit, StackBlitz (in addition to SSH),
  so the existing tunnel hint at least fires in those
  environments.
* ``_parse_pasted_callback`` accepts any of: a full
  ``http(s)://...?code=...&state=...`` URL, a bare ``?code=...``
  query string, a bare ``code=...&state=...`` fragment, or a
  bare opaque code value.  Returns the same dict shape the HTTP
  callback handler produces, so the caller's state / error
  validation works unchanged (no CSRF bypass).
* ``_prompt_manual_callback_paste`` reads stdin with a clear
  multi-line explanation of what's happening and what to paste.
* ``_xai_oauth_loopback_login`` gains a ``manual_paste`` kwarg
  that skips the HTTP listener entirely.  The redirect_uri,
  PKCE verifier, state, and nonce are byte-identical to the
  loopback path so xAI's token endpoint can't tell the
  difference at the protocol level.
* ``_print_loopback_ssh_hint`` now also mentions
  ``--manual-paste`` so users without a real SSH client see a
  path forward instead of a dead-end tunnel recipe.
* ``_login_xai_oauth`` threads ``args.manual_paste`` into the
  loopback helper.
2026-05-18 20:10:52 -07:00
yannsunn
1d6f3753de feat(proxy): add xai upstream adapter for Grok via OAuth 2026-05-18 20:09:32 -07:00
Beandon13
bde6313e34 feat(kanban): archive --rm to hard-delete archived tasks
Salvages #19964 by @Beandon13. Adds `hermes kanban archive --rm` to
permanently remove already-archived tasks with cascading cleanup of
links, comments, events, runs, and notify-subs. Safety guard: only
archived tasks can be deleted; active/blocked/done must be archived
first.

Cherry-picked from #19964 onto current main (severe stale base, applied
manually to preserve substance only).
2026-05-18 20:09:26 -07:00
xxxigm
60ef368792 fix(xai-oauth): split 403 (tier/entitlement) from 400/401 in token endpoint
xAI's token endpoint returns HTTP 403 to the OAuth grant when the
account isn't on the allowlist for API access (e.g. standard
SuperGrok subscribers — see #26847). Treating it like a stale-token
400/401 made ``format_auth_error`` append "Run ``hermes model`` to
re-authenticate", which is misleading because re-login can't change
xAI's tier decision.

Split 403 off in both ``refresh_xai_oauth_pure`` and the loopback
login token exchange:

* New error code ``xai_oauth_tier_denied`` with ``relogin_required=False``
* Message explains the entitlement gate and points at the
  ``XAI_API_KEY`` + ``provider: xai`` fallback
* 400/401 still set ``relogin_required=True`` as before
* 5xx still set ``relogin_required=False`` as before
2026-05-18 20:08:09 -07:00
iqdoctor
4229facc01 docs(windows): avoid piping installer directly into iex 2026-05-18 20:05:47 -07:00
houenyang-momo
50158a60f9 fix(tui): improve charizard completion menu contrast 2026-05-18 20:05:23 -07:00
zccyman
2e09d2567c feat(kanban): add auto_promote_children config toggle
When the kanban auto-decomposer fans a triage task into child tasks,
recompute_ready() immediately promotes parent-free children to 'ready'
so the dispatcher picks them up. Some users want a manual workflow
where children stay in 'todo' for review before dispatch.

Add 'kanban.auto_promote_children' config key (default: true):
- false: children stay in 'todo' after decomposition
- true: existing behavior (auto-promote to 'ready')

Changes:
- kanban_db.py: decompose_triage_task() gains auto_promote param
- kanban_decompose.py: reads auto_promote_children from config
- kanban dashboard API: exposes the new setting in GET/PUT /orchestration

Closes #28016
2026-05-18 20:04:32 -07:00
EloquentBrush0x
b3e714e8b7 fix(xai-oauth): quarantine dead tokens on terminal refresh failure
resolve_xai_oauth_runtime_credentials() called _refresh_xai_oauth_tokens()
with no try/except. A terminal refresh failure (HTTP 400/401/403 —
invalid_grant, token revoked) propagated without clearing the dead
access_token / refresh_token from auth.json, causing every subsequent
session to retry the same doomed network request.

Add a try/except around the refresh call that mirrors the existing
credential_pool.py quarantine: when _is_terminal_xai_oauth_refresh_error
identifies a non-retryable failure, clear the dead token fields from
auth.json and write a last_auth_error diagnostic marker so future calls
fail fast with a clear relogin_required error instead of hitting the
network.

active_provider is preserved (set_active=False) so multi-provider users
whose chosen provider is not xai-oauth are unaffected.

Tests: two new cases in test_auth_xai_oauth_provider.py cover terminal
quarantine and transient pass-through.
2026-05-18 20:02:11 -07:00
hehehe0803
87ace43f1e fix(aux): remove stale session_search model menu entry 2026-05-18 20:01:34 -07:00
zccyman
4e9df52d60 fix: elevate plugin discovery failures from debug to warning
Plugin discovery exceptions in gateway startup (gateway/run.py) and
CLI startup (hermes_cli/main.py) are caught and logged at DEBUG
level, making them invisible at the default INFO log level.

If any plugin import fails — syntax error, missing dependency, import
cycle — operators get zero indication unless they bump the log level
to DEBUG. This makes broken plugins appear enabled but silently
non-functional.

Change both locations to logger.warning() so failures are visible at
production log levels.

Closes #28137
2026-05-18 19:35:41 -07:00
rudi193-cmd
95846eddd2 fix(auth): treat empty credential pool entries as unauthenticated
Fixes #28140

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:33:07 -07:00
Austin Pickett
2ef501e1f5
feat(cli): add /update slash command to CLI and TUI (#23854)
* feat: add /update slash command to CLI and TUI

* test(cli): add Python tests for /update slash command

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address Copilot review for /update slash command

Route classic CLI /update through prompt_toolkit modal confirmation and
defer relaunch to the main-thread cleanup path after app.exit(). Tighten
Y/n semantics, add Python wrapper and catalog coverage tests, and assert
/update stays visible in the TUI command catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address review feedback on /update command

- Replace raw input() with _prompt_text_input_modal in _handle_update_command
  to avoid EOF/hang/keystroke-leak races with prompt_toolkit's stdin ownership
- Fix confirmation logic: only proceed on recognized affirmative aliases
  (y/yes/1/ok); cancel on everything else including empty string, typos,
  and unrecognized input — matches all other [Y/n] prompts in the codebase
- Route relaunch through main-thread shutdown path: set _pending_relaunch
  and return False from process_command so process_loop triggers app.exit();
  run() then calls relaunch() after prompt_toolkit has restored terminal modes
  and after cleanup — safe on both POSIX (execvp) and Windows (subprocess+exit)
- Fix misleading docstring in test_update_command.py: the Vitest only covers
  the TypeScript slash handler that emits code 42, not the Python wrapper
  branch that acts on it
- Rewrite tests to use SimpleNamespace pattern (like test_destructive_slash_confirm)
  so _prompt_text_input_modal can be stubbed directly
- Add Python test for _launch_tui exit-code-42 → relaunch branch in main.py

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* fix(cli): polish test fixtures for /update command

- Remove unused _prompt_text_input from SimpleNamespace stub
- Use pytest.fail sentinel in managed-install guard test to catch unexpected modal invocations

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* chore: re-trigger CI after Copilot review fixes

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>
2026-05-18 20:10:46 -04:00
falasi
43802ef3e3 fix: add default base_url_override for ollama-cloud provider 2026-05-18 14:31:37 -07:00
Gianfranco Piana
bb9ecb2178 feat: add cron job profile support 2026-05-18 17:39:50 +00:00
EloquentBrush0x
d9331eecee fix(minimax-oauth): quarantine dead tokens on terminal refresh failure
resolve_minimax_oauth_runtime_credentials called _refresh_minimax_oauth_state
without a try/except, so a terminal failure (invalid_grant,
refresh_token_reused, invalid_refresh_token) raised AuthError but left
the dead refresh_token in auth.json. Every subsequent API call retried
the same token via a network round-trip, failing identically each time.

Fix: wrap the refresh call and, when exc.relogin_required is True and a
refresh_token is present, clear the dead OAuth fields (access_token,
refresh_token, expires_*) and write a last_auth_error quarantine marker
to auth.json before re-raising. The next call sees no access_token and
fails fast with 'not_logged_in' — no network retry — and the user is
prompted to re-authenticate.

Mirrors the existing quarantine pattern for Nous (_quarantine_nous_oauth_state),
xAI-OAuth (#28116), and Codex-OAuth (#28118). Persist failure is
best-effort (logged at DEBUG, error still re-raised).

Salvaged from #28003 by @EloquentBrush0x — contributor's branch was
severely stale (would have reverted ~5000 LOC across azure/kanban/i18n
subsystems); fix re-applied surgically with their pattern preserved and
added two regression tests (terminal-quarantines + transient-does-not-quarantine).
2026-05-18 10:34:03 -07:00
EloquentBrush0x
b570e0fdd0 fix(codex-oauth): quarantine terminal refresh errors so dead tokens are not replayed across sessions
When a Codex OAuth refresh token is permanently invalidated (HTTP 400/401/403,
token revoked or reused), _mark_exhausted was called but auth.json was left with
the dead credentials. On the next session, _seed_from_singletons re-read
auth.json and re-seeded the pool with the same revoked token, triggering the
same terminal failure in a loop.

Add _is_terminal_codex_oauth_refresh_error to auth.py and a matching quarantine
block in _refresh_entry: when a terminal error is detected and auth.json holds
no newer tokens, clear access_token/refresh_token from auth.json and remove all
device_code-sourced pool entries from memory. Mirrors the Nous quarantine added
in c90556262 and the xAI quarantine in #28116.

Also add a pre-refresh sync from auth.json before calling refresh_codex_oauth_pure,
matching the xAI and Nous patterns, to avoid refresh_token_reused races when
multiple Hermes processes share the same auth.json singleton.

Salvaged from #27911 by @EloquentBrush0x — contributor's branch was severely
stale (would have reverted ~5000 LOC across azure/kanban/i18n subsystems);
fix re-applied surgically on current main with their predicate and tests preserved.
2026-05-18 10:31:40 -07:00
Teknium
9aae59feab
fix(compress): make abort-on-summary-failure opt-in via config flag (#28117)
PR #28102 made the summary-failure abort path the unconditional default,
changing established behavior. Gate it behind config.yaml flag
`compression.abort_on_summary_failure` (default False = historical
fallback-placeholder behavior).

- hermes_cli/config.py: new `compression.abort_on_summary_failure` key,
  default False, documented inline.
- agent/agent_init.py: read the flag from compression config and pass to
  ContextCompressor.
- agent/context_compressor.py: `__init__` accepts `abort_on_summary_failure`
  (default False). `compress()` failure branch gates the abort on the
  flag; when False, falls through to the restored legacy fallback path
  (static "summary unavailable" placeholder + drop middle window).
- tests: restore original fallback expectations as default; add new
  TestAbortOnSummaryFailure class for the opt-in mode.

Gateway/CLI plumbing (force=True on /compress, hygiene/handler abort
detection, locale `gateway.compress.aborted` key) from PR #28102 stays
intact — those paths only fire when `_last_compress_aborted` is True,
which now only happens when the flag is enabled.
2026-05-18 10:28:20 -07:00
EloquentBrush0x
5e40f83cb7 fix(xai-oauth): quarantine terminal refresh errors so dead tokens are not replayed across sessions
When refresh_xai_oauth_pure raises a terminal error (HTTP 400/401/403,
i.e. revoked or reused refresh token), _refresh_entry's existing race-
recovery path re-syncs from auth.json and returns if another process has
already rotated the tokens.  If auth.json still holds the same stale
token pair, the function fell through to _mark_exhausted — leaving the
dead credentials in auth.json.  On the next Hermes startup _seed_from_singletons
re-seeded the pool from those stale tokens, causing the same failure loop
on every session.

Fix: after the auth.json re-sync check in the xAI-oauth error handler,
detect terminal errors with the new _is_terminal_xai_oauth_refresh_error
helper and apply a quarantine:
- Clear access_token and refresh_token from providers["xai-oauth"]["tokens"]
  in auth.json so they are not re-seeded.
- Write a last_auth_error entry for hermes doctor / auth status diagnostics.
- Remove all loopback_pkce entries from the in-memory pool so the current
  session stops retrying with the dead credentials.

Mirrors the identical quarantine already in place for Nous OAuth
(c90556262).

Closes the parity gap introduced when c90556262 added Nous-only terminal
error handling without a corresponding xAI-oauth path.
2026-05-18 10:28:09 -07:00
konsisumer
226680500d fix(auth): improve xAI OAuth SSH hint with visual header and auto-detected host 2026-05-18 10:26:55 -07:00
briandevans
bf6eeb3f93 fix(xai-oauth): show "not received" page when loopback callback has no code
When xAI's auth backend fails to redirect (e.g. the German "We couldn't reach
your app" fallback shown in #27385), users sometimes navigate manually to the
bare loopback callback URL — `http://127.0.0.1:<port>/callback` with no query
string. The handler used to return 200 "xAI authorization received" for any
GET that hit the expected path, because `parse_qs("")` yields no `code` and no
`error`, leaving `result` untouched while the success page was still served.

The CLI's wait loop, of course, still saw no code and timed out with
`AuthError: xAI authorization timed out waiting for the local callback.`
The user is left looking at a browser tab that claims success and a terminal
that says failure — exactly the contradiction in #27385.

This change makes the empty-callback case return 400 with an explicit
"not received" page and a hint to retry `hermes auth add xai-oauth`. The
wait-loop semantics are unchanged: `result["code"]` and `result["error"]`
both stay None, so the CLI still raises a real timeout rather than treating
the bare hit as a successful callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:26:00 -07:00
Fewmanism
0d63661702 fix: latch xAI OAuth callback result 2026-05-18 10:23:13 -07:00
Fewmanism
eac198b6d5 fix: make xAI OAuth callback server threaded 2026-05-18 10:23:13 -07:00
glennc
9df9816dab feat(azure-foundry): add Microsoft Entra ID auth
Use azure-identity DefaultAzureCredential for keyless Foundry auth.

Preserve refreshable callable credentials through OpenAI and Anthropic client paths.

Add setup, doctor, auth status, docs, and tests for Entra auth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-18 10:14:38 -07:00
EloquentBrush0x
502d03d5a3 fix(kanban): detect cycles in decompose_triage_task sibling-link pre-validation
decompose_triage_task inlines SQL INSERTs for atomicity and intentionally
bypasses link_tasks() — which calls _would_cycle() per edge.  If the LLM
emits a cyclic parent graph (e.g. A.parents=[1], B.parents=[0]) the DB
write succeeds but every involved child deadlocks in 'todo' forever:
recompute_ready() requires all parents to be done, which is impossible
when A waits for B and B waits for A.

Add a Kahn topological sort over the sibling parent indices in the
pre-validation block, before any DB writes.  Mirrors the cycle-safety
guarantee that link_tasks() provides for manually linked tasks.
2026-05-18 09:40:44 -07:00
Siddharth Balyan
e3a254d65b
feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection (#27845)
* feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection

dep_ensure.py gains Windows awareness: PowerShell invocation, platform-
specific browser detection, (path, shell) tuple returns.

install.ps1 gains -Ensure/-PostInstall modes using npm -g --prefix
(aligned with install.sh) and agent-browser install for Chromium.

browser_tool.py gains node/ in candidate dirs for Windows .cmd shims.
Both install scripts bundled in pip wheel.

Tracking: #27826

* fix(install.ps1): add --ignore-scripts to npm install for camofox

@askjo/camofox-browser has a dependency (impit) whose postinstall
script runs `npx only-allow pnpm`, which fails under npm. Adding
--ignore-scripts avoids the spurious failure without affecting
functionality.

Tracking: #27826

* fix: remove duplicate install scripts from git

CI already copies scripts/install.{sh,ps1} into hermes_cli/scripts/
during wheel build. No need to commit copies — .gitignore keeps them
out, _find_install_script() falls back to scripts/ for git-clone users.

Tracking: #27826

* fix: address review — remove env_extra, fix ps1 error handling

- Remove unused env_extra parameter from ensure_dependency()
- Invoke-EnsureMode node case now uses Test-Node consistently
- Install-AgentBrowser uses throw instead of exit 1
2026-05-18 16:34:24 +05:30
Siddharth Balyan
6f5ec929a1
feat(config): add install-method stamping + Docker detection (#27843)
* feat(config): add install-method stamping + Docker detection

Dockerfile stamps "docker", install.sh stamps "git", and cmd_postinstall
stamps "pip" into ~/.hermes/.install_method. detect_install_method() reads
the stamp first, then falls back to managed-system / container / .git
heuristics. Adds Docker upgrade guidance.

Tracking: #27826

* fix(stamp): move Docker stamp to entrypoint, install.sh stamp after print_success

The Dockerfile stamp was overwritten by the VOLUME overlay at container
start. Moving it to entrypoint.sh ensures it persists. The install.sh
stamp now writes after print_success so it only lands on full success.
2026-05-18 16:34:10 +05:30
Teknium
f2fdb9a178
feat(gateway): deliverable mode — ship artifacts as native uploads from any agent surface (#27813)
The agent can now produce a chart, PDF, spreadsheet, or any other supported
file type and have it land in Slack / Discord / Telegram / WhatsApp / etc.
as a native attachment, just by mentioning the absolute path in its
response. Same primitive works for kanban-worker completions: workers
attach artifacts via kanban_complete(artifacts=[...]) and the gateway
notifier uploads them alongside the completion message.

Changes:

- gateway/platforms/base.py: extract_local_files now covers PDFs, docx,
  spreadsheets (xlsx/csv/json/yaml), presentations (pptx), archives
  (zip/tar/gz), audio (mp3/wav/...), and html — not just images and video.
  Image/video extensions still embed inline; everything else routes to
  send_document via the existing dispatch partition in gateway/run.py.

- tools/kanban_tools.py + hermes_cli/kanban_db.py: kanban_complete gains
  an explicit ``artifacts`` parameter. The handler stashes it in
  metadata.artifacts (for downstream workers) and the kernel promotes
  it onto the completed-event payload so the notifier can find it
  without a second SQL round-trip.

- gateway/run.py: _kanban_notifier_watcher now calls a new helper
  _deliver_kanban_artifacts after sending the completion text. The
  helper reads payload.artifacts (preferred), falls back to scanning
  the payload summary and task.result with extract_local_files, then
  partitions images / videos / documents and uploads each via
  send_multiple_images / send_video / send_document.

- website/docs/user-guide/features/deliverable-mode.md + sidebars.ts:
  user-facing docs page covering the extension list, the kanban
  artifacts pattern, and the MCP-for-connector-breadth recommendation.

Tests:

- tests/gateway/test_extract_local_files.py: 7 new test cases
  (documents, spreadsheets, presentations, audio, archives, html,
  chart-pdf canonical case). 44 passing, 0 regressions.
- tests/tools/test_kanban_tools.py: 4 new cases covering the artifacts
  arg shape (list / string / merge with existing metadata / type
  rejection). 17 passing.
- tests/hermes_cli/test_kanban_notify.py: 2 new cases covering full
  notifier → artifact-upload path and missing-file silent-skip. 12
  passing.
- E2E (real files, real kanban kernel, real BasePlatformAdapter):
  worker calls kanban_complete(artifacts=[png,pdf,csv]) → metadata +
  event payload land → notifier helper partitions correctly →
  send_multiple_images called once with the PNG, send_document called
  twice with PDF + CSV.

What's NOT in this PR (deferred to follow-ups):

- Ad-hoc "research this for two hours, ping the thread when done"
  slash command — covered today by kanban subscriptions; a dedicated
  slash command can ride a follow-up PR if needed.
- Setup-wizard prompt for recommended MCP servers (Notion, GitHub,
  Linear, etc.) — docs page lists them; UI is a separate change.

Plan and rationale captured in ~/.hermes/docs/perplexity-computer-parity.pdf
(local doc, not shipped).
2026-05-18 02:14:43 -07:00
Teknium
dadc8aa255
fix(kanban): surface unusable triage auxiliary model (auto-decompose aware) (#27871)
Adds a 'triage_aux_unavailable' diagnostic for tasks stuck in triage when
neither the active aux helper slot nor the main-model auto fallback is usable.

Auto-decompose aware:
- kanban.auto_decompose=True (default): primary is auxiliary.kanban_decomposer,
  triage_specifier is the fanout=false fallback.
- kanban.auto_decompose=False: primary is auxiliary.triage_specifier (manual
  'hermes kanban specify' path).

Default aux slots use 'provider: auto' which falls back to the main model, so
this rule only fires when both the explicit slot config AND the main-model
auto fallback are absent. Quiet by default; informative when there is a real
config gap.

Also adds kd.config_from_runtime_config() that carries kanban + auxiliary +
model keys through to diagnostics, and updates CLI/dashboard call sites to
use it. config_from_kanban_config() is preserved for back-compat.

Reworks the original PR #25640 idea (@qWaitCrypto) to align with the new
auto-decompose dispatcher path landed in #27572. The original PR pointed only
at auxiliary.triage_specifier, which is now the fallback rather than the
primary helper.

Co-authored-by: qWaitCrypto <axmaiqiu@gmail.com>
2026-05-18 01:27:06 -07:00
qWaitCrypto
d9fef0c8ab fix(kanban): align failure diagnostics with retry limit 2026-05-18 01:22:16 -07:00
qWaitCrypto
6e60a8a092 feat(kanban): make worker log retention configurable 2026-05-18 01:21:41 -07:00