Commit Graph

20312 Commits

Author SHA1 Message Date
kshitij 83314ca381 fix(update): make the ZIP replace atomic across all entries + dedupe venv layout
Closes #76104, closes #76105.

#76104 — `_atomic_replace_dir` (#49145) made each individual directory swap
safe, but `_update_via_zip` replaced ~70 top-level entries in a loop with no
atomicity across iterations. `agent/` lands at os.listdir index 13 and
`tools/` at 66, so an interruption between them left the new
`agent/context_compressor.py` (module-level `from tools.todo_tool import
TODO_INJECTION_HEADER`) beside a stale `tools/todo_tool.py` — every file
valid Python, the tree unbootable. That is the mechanism behind the
ImportError fixed in #76091, and the "partial update" field report in #63717.

Split into stage-all-then-swap-all:
  - `_stage_replacement` copies each dir to a sibling staging path, touching
    nothing live, so a failure during the long copy phase is a no-op.
  - `_commit_staged_replacements` performs the renames and, if any fails,
    restores every entry already swapped — the tree lands wholly new or
    wholly old, never mixed.
This shrinks the failure window from a full tree copy to N renames and makes
what remains recoverable. Added an up-front free-space check, since staging
needs a second copy of the tree; a clear error beats running out mid-swap.

#76105 — venv interpreter resolution was open-coded in 7 places across 4
files using 3 different Windows predicates. #76091 added the seventh because
the correct behaviour lived 2400 lines away. Hoisted `venv_bin_dir()` /
`venv_python_path()` into hermes_constants (import-safe, no new imports) and
routed every site through them; `managed_uv._venv_python` now delegates so
its 6 callers are untouched.

`_atomic_replace_dir` is retained — it is re-exported from main.py and has
its own #49145 regression test; removing it is out of scope here.

Tests: 10 new (rollback-on-mid-swap-failure is mutation-verified — it fails
when the rollback loop is removed), plus a guard that fails if a new call
site hand-rolls Scripts/bin again. E2E-verified against the real staging +
commit helpers with a live tree.
2026-08-01 16:45:00 +05:30
kshitij 15cb86eba3 refactor(update): one definition of "first-party module"
/simplify-code reuse reviewer (HIGH): the probe and the user-facing hint
each carried their own hand-written list of first-party package roots,
and they had already diverged on day one —

  module      probe   hint
  cli         False   True    <- rollback with no explanation
  hermesx     True    False   <- third-party blamed on our updater

Hoist a single FIRST_PARTY_MODULE_ROOTS + is_first_party_module() into
hermes_constants (import-safe, no new imports) and have both consume it;
the probe gets the set injected into its source rather than re-typing it.
Also completes the roster — cron, utils, run_agent, model_tools,
toolsets, tui_gateway, acp_adapter were missing from both copies.

Verified by executing the real probe source against 19 module roots:
0 disagreements. Added a test that fails if either side grows a private
copy again.
2026-08-01 16:09:27 +05:30
kshitij bf18710a54 fix(update): make the git-path import check non-destructive
Phase 2 review (C2) and /simplify-code findings.

C2 — the git path ran the import guard before `_clear_bytecode_cache`,
wired into the syntax guard's `git reset --hard` rollback. But
`cannot import name 'X'` is ALSO the documented signature of the
stale-bytecode class (#6207, #60242, see
_sweep_stale_bytecode_if_checkout_changed), which the very next steps —
and the launch-time sweep — already self-heal. A false positive there
would destroy a good update over a state that fixes itself.

Remove the guard from the rollback path entirely and re-add it at the
end of the git path, after bytecode sweep + dependency reinstall + lazy
refresh, as a WARNING only. By then every benign source of a transient
ImportError has run, and we never reset the user's checkout.

W6 — the headline regression test was vacuous: it patched
`hermes_main._UPDATE_CRITICAL_FILES`, but the syntax guard reads
`update_cmd`'s global, so the stub files were never examined and the
(True, None, None) came from "no files found" rather than "parses
clean". Patch the right module; mutation-checked (the test now fails
when the guard is disabled).

S5 — `startswith(("tools","agent","hermes","gateway"))` also matched
third-party `agents`/`agentops`/`toolsets`. Compare the first dotted
segment against an exact set instead.

S6 — hoist the per-line ChatConsole() instantiation.
2026-08-01 16:09:27 +05:30
kshitij 822571fa8e fix(update): don't roll back a good update over uninstalled deps
Phase 2 review caught a false-rollback I introduced: on the git path the
import guard runs at the post-pull syntax check, which is BEFORE the
dependency sync. A release that adds a new third-party requirement would
fail the probe and trigger `git reset --hard` on a perfectly good update.

Rather than reorder the git path (the guard belongs with the rollback it
feeds), make the probe ignore a missing module that isn't ours. A missing
third-party package means deps aren't installed yet; a missing first-party
module means the update dropped a file, which IS the skew we're hunting.

This also makes the ZIP path's ordering non-load-bearing.

Verified: third-party absent -> (True, None, None); first-party absent ->
flagged; and the original TODO_INJECTION_HEADER skew is still caught.
2026-08-01 16:09:27 +05:30
kshitij aa5d4fd6ee fix(update): probe the venv interpreter, not the driving one
Self-review against the sibling probe `_venv_core_imports_healthy`
surfaced this: that helper deliberately resolves the project venv's
python rather than using `sys.executable`, because `hermes update` may
be driven by a different interpreter than the install's own.

The new import guard had the same requirement and missed it. Probing
`sys.executable` would validate a tree the user never actually runs —
and that divergence is most likely on Windows, the exact platform this
guard was added for.

Falls back to the running interpreter when there is no venv (normal in
a dev checkout). Regression test asserts the venv python is chosen; it
fails when the fix is reverted.
2026-08-01 16:09:27 +05:30
kshitij baecc840e5 fix(update): catch partially-updated trees that parse but can't import
A Windows user reported every startup dying with `ImportError: cannot
import name 'TODO_INJECTION_HEADER' from 'tools.todo_tool'`. The symbol
exists on main; their tree had the new `agent/context_compressor.py`
(which imports it at module level) alongside a pre-update
`tools/todo_tool.py`.

The post-update guard missed it. `_validate_critical_files_syntax` only
py_compiles files, and every file in a skewed tree parses fine — it is
the combination that is broken. The guard reported success and the
update completed over an install that could not start.

The ZIP-update path (Windows-only, used when git file I/O is broken)
is where the skew comes from: its copy loop replaces top-level entries
one at a time in `os.listdir` order, so `agent/` lands at index 13 and
`tools/` at index 66. Any failure between them leaves exactly this
mismatch — and that path had no post-copy validation or rollback at all.

- Add `_validate_critical_modules_import`: imports the four startup
  modules in a subprocess (~0.4s) so cross-module breakage is caught.
  Non-import errors (config/env) are ignored; a probe that cannot spawn
  is non-fatal so we never block an update on our own tooling.
- Run it after the syntax guard on the git path, reusing the existing
  auto-rollback.
- Run it on the ZIP path after dependency install (so a genuinely-new
  requirement is not misreported as a partial copy), and make the ZIP
  failure message state the install may be half-updated.
- Add `partial_update_hint()` and print it under "Failed to initialize
  agent", so users see "re-run hermes update" instead of a bare
  ImportError. Stays silent for ModuleNotFoundError and third-party
  imports, which need different remediation.

Verified by simulating the exact skew: the syntax guard returns ok=True
while the import guard returns the user's error verbatim.
2026-08-01 16:09:27 +05:30
kshitijk4poor 85e0073902 refactor(compression): fold simplify-pass findings into feasibility skip
- Reuse telemetry['middle_window_tokens'] for the skip's middle estimate
  (is-None fallback to a fresh estimate) so log and telemetry agree
- Declare prellm_skip_count in the base telemetry schema (fixed shape)
- Defer _derive_auto_focus_topic into the non-skip branch (user-turn scan
  was wasted work on every skip)
- Document the skip in compress()'s Algorithm list and force: arg doc
- Drop dead call_llm patches from 7 tests (unreachable with
  _generate_summary mocked)
2026-08-01 16:00:52 +05:30
TRON 8daf03063d fix(compression): add pre-LLM feasibility check to skip costly no-op summaries
When the middle section is < 10% of threshold tokens AND at least one prior
real-usage ineffectiveness strike has been recorded, skip the expensive LLM
summarization call and fall through to the deterministic message-dropping
path.  Without this guard, a tool-heavy session where the protected tail
already holds most of the tokens can burn 500+ seconds on a summary call
that replaces a few lightweight messages with negligible token savings.

Key design decisions per GottZ review on PR #60451:

1. Separate _prellm_skip_count counter — never increments
   _ineffective_compression_count (the strike counter that latches at >=2
   to disable compression entirely).  One real strike + one skip must NOT
   permanently lock out compression until /new.

2. feasibility_skip sentinel flag — exempts skips from the abort branch
   (abort_on_summary_failure / _last_summary_auth_failure /
   _last_summary_network_failure).  A stale failure flag from a prior
   cycle must not turn a deliberate skip into a full abort.

3. reason=None for feasibility-skip fallbacks — a stale _last_summary_error
   from an earlier real failure must not be embedded into the skip's
   deterministic fallback marker.

4. info-level logging for feasibility-skip fallbacks (not warning) — this
   is an intentional optimization, not a failure.

Skipped when force=True (manual /compress) so auth/error handling paths
are always exercised on explicit user request.

Adds 6 regression tests (TestPreLlmFeasibilityCheck) covering:
- Strike counter isolation
- Stale auth/network failure flag immunity
- force=True bypass
- No-skip when no prior strikes
- Counter reset on session reset

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: TRON <tron-agent@agentmail.to>
2026-08-01 16:00:52 +05:30
fyzanshaik d50858584c fix(tui): preserve queued image ownership 2026-08-01 15:58:42 +05:30
kshitij 5b3c66a04f chore: add carlotestor to contributor email directory
Required for check-attribution CI on salvage PR #76083.
2026-08-01 15:58:25 +05:30
kshitij 321cbcc2e2 test(redact): add YAML ReDoS test, strengthen existing test with keyword
- Relax timing bound from 1.0s to 2.0s (CI machine robustness).
- Add test_long_dotted_run_with_keyword_completes_fast: includes a secret
  keyword so the pre-gate does NOT skip _CFG_DOTTED_RE — directly exercises
  the possessive-quantifier regex, not just the pre-gate.
- Add test_yaml_assign_redos_resistance: _YAML_ASSIGN_RE was modified but
  had no ReDoS test — add 100-line stress input.
- Add test_yaml_assign_secret_still_redacted: verify YAML matching behavior
  preserved with possessive quantifiers.
2026-08-01 15:58:25 +05:30
carlotestor 13ad903a3c perf(redact): eliminate exponential backtracking in config-key patterns
_CFG_DOTTED_RE's nested quantifier (?:[A-Za-z0-9_\-]+\.)+ backtracks
exponentially on long non-matching dotted runs (doubles every ~4
segments). Flatten it and use possessive quantifiers (py3.11+) in
_CFG_DOTTED_RE and _YAML_ASSIGN_RE wherever the successor is disjoint.

Zero behavior change: equivalence fuzz-verified over 120k structured
and random inputs comparing full sub() output including groups. Adds a
ReDoS regression test.
2026-08-01 15:58:25 +05:30
kshitijk4poor 84952e89f9 chore: map tron@chriswykel.com -> Wpnx330 (PR #68334 salvage) 2026-08-01 15:43:13 +05:30
kshitij 536754919d fix(auxiliary): replan cache sections on the async fallback path too
_call_fallback_candidate_sync replans messages/tools for each resolved
destination, but its async mirror still shipped the caller's decorated
sections verbatim — the primary destination's markers (including a
direct-native tool marker) leaked to fallback candidates with different
cache contracts, and the relay saw the display label instead of the
resolved provider/api_mode. Mirror the sync path: resolve the
destination, replan both sections, thread provider/api_mode through
_relay_async_completion, and replan again for the auth-refresh retry
client. Mutation-checked: the new parity test fails on the verbatim
pass-through shape.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
kshitij e7340ea281 perf(prompt-caching): make PromptCachePlan.marker_count lazy
The count walked every message part and tool schema on every request but
is consumed only by tests. Compute it on demand via a property instead.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
kshitij af06308425 refactor(prompt-caching): collapse triplicated destination-plan and label parsing
Three copies of the same logic landed with #76032:
- MoA's _call_prepared_aggregator and auxiliary_client's
  _replan_synchronous_cache_sections both implemented stub → policy →
  strip → plan for a resolved destination. Extract
  plan_cache_sections_for_destination() into agent_runtime_helpers (which
  already owns the policy functions) and route both through it. Also
  removes a redundant full-transcript deepcopy+strip per request (the
  caller pre-stripped what build_prompt_cache_plan strips again).
- The fallback_chain[N] label regex + chain-entry lookup lived in
  _fallback_entry_timeout AND _fallback_destination. Extract
  _fallback_chain_entry() and reuse.

MoA's cache-plan failure log is promoted debug → warning: the call-block
site skips MoA, so this block is the aggregator's only decoration path —
a silent failure ships an undecorated request (the 0%-cache MoA bug class).

Behavior-preserving; 195 targeted tests green.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
kshitij 7ae4a5efba fix(prompt-caching): consolidate static-prefix split, guard empty volatile suffix
_apply_static_prefix_marker duplicated _apply_system_cache_markers' split
logic minus its empty-suffix guard: when the stored system prompt equals
the static prefix exactly, the tool-cache plan emitted a two-part split
with a trailing empty text block — HTTP 400 on native Anthropic. Fold the
tool-cache layout into the existing helper via mark_suffix /
fallback_to_whole flags; the empty-suffix case now marks the prompt as
one whole block. Behavior-parity verified against the merged planner for
every non-empty-suffix shape.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
kshitijk4poor e078c8c6ef fix: widen fallback warning to the sibling custom-endpoint 256K path
Review pass 2 (reuse reviewer HIGH): the step-3b probe-down fallback for
custom/local endpoints returns the same silent 256K default but only
logged at INFO - invisible by default, and it is the MORE common path
for small local models (the exact users the warning exists for).

Extract _warn_context_length_fallback() (deduped per model+base_url)
and call it from both fallback sites, per the fix-the-whole-bug-class
rule. Regression test drives the custom-endpoint path and fails without
the widening (mutation-checked).
2026-08-01 15:05:05 +05:30
kshitijk4poor 4c2d0c7fd8 refactor: dedupe fallback warning per model, drive pool-cleanup tests through real run()
Review follow-up:
- Warn once per (model, base_url) at the step-9 fallback via a module-level
  dedup set (established _WARNED_* idiom). The fallback result is
  deliberately never cached, so the un-deduped warning fired on every
  resolution - e.g. once per gateway message via the session-hygiene path.
- Replace the three inline-mock pool-cleanup tests (which reproduced the
  try/except block against a MagicMock and passed even with the production
  code reverted) with a parametrized test that drives the real
  BatchRunner.run() with a patched Pool; drop the CPython stdlib
  signature change-detector test.
- Add a once-per-model warning regression test; clean up dead imports.

All tests verified to fail against pre-PR batch_runner.py/model_metadata.py
and pass with the fix (mutation check).
2026-08-01 15:05:05 +05:30
kshitijk4poor a1ff62a139 fix: context-length fallback logging, batch trajectory durability, pool cleanup
Salvage of #6629 by aaronlab (kshitijk4poor reworked against current main).

Three concerns from the original PR, reworked to address review feedback:

1. Context-length fallback diagnostic (agent/model_metadata.py):
   get_model_context_length() silently returned 256K when all 9 detection
   methods failed. Users with small-context models (8K, 32K) would get 256K
   silently, causing hard-to-debug API context-length errors. Added a
   warning log at the step 9 fallback with model name, base_url, and the
   correct config override hint (model.context_length, not context_length).
   The token-estimation ceiling-division fix from the original PR already
   landed on main (5c2ecdec) with CJK handling — not duplicated here.

2. Fsync for batch trajectory writes (batch_runner.py):
   Trajectory entries were written without flush/fsync, but the checkpoint
   immediately marked them as completed. A crash between write and disk
   sync would leave the checkpoint claiming completion with no trajectory
   data on disk. Added flush() + os.fsync() before checkpoint update.

3. Pool cleanup on interruption (batch_runner.py):
   Ctrl+C during pool.imap_unordered() relied on context manager cleanup
   which can hang. Added explicit pool.terminate() + pool.join() for both
   KeyboardInterrupt and Exception paths. The original PR used
   pool.join(timeout=10) which is invalid — CPython's Pool.join() takes
   no timeout parameter. Fixed to use pool.join() without arguments.

Tests:
  - test_warning_emitted_on_fallback: verifies warning fires at step 9
  - test_no_warning_when_cached: verifies no false warning when cache hits
  - test_trajectory_entry_is_synced_to_disk: verifies os.fsync is called
  - test_pool_terminate_called_on_exception: verifies cleanup on RuntimeError
  - test_pool_terminate_called_on_keyboard_interrupt: verifies cleanup on Ctrl+C
  - test_pool_join_called_without_timeout: verifies no timeout arg to join()
  - test_real_pool_join_accepts_no_timeout: integration check on CPython API

Co-authored-by: Aaron Lab <aaronlab@users.noreply.github.com>
2026-08-01 15:05:05 +05:30
ajzrva-sys 34c11fa689 fix(terminal): honor explicit config keys over stale env
Let terminal keys explicitly present in config.yaml override matching stale TERMINAL_* values while preserving environment values for omitted keys. Merged defaults remain backfill-only.

Exercise the real config.yaml to _get_env_config path for backend selection, partial terminal sections, matching-key overrides, environment fallback, one-shot bridging, and config read failures.

Closes #71137
2026-08-01 15:03:42 +05:30
Rod Boev 9fc12bf7a4 perf(prompt-caching): preserve tool-loop cache boundaries (#20880) 2026-08-01 14:27:12 +05:30
hans f09c56c003 fix(gateway): dedup model-switch markers so they don't accumulate in history
Each mid-session `/model` switch appended a `[System: The active model for
this chat has changed to …]` user-role marker to the live conversation history
and never removed the prior one. N switches left N stale markers, all re-sent
to the provider on every subsequent turn — the issue measured ~80-120 tokens
each (Chinese+English), so 5 MoA-preset switches burned ~400-600 context
tokens per API call, permanently. Only the newest marker is meaningful (it
names the currently-active model); the rest are pure waste. Fixes #65891.

`_append_model_switch_marker` now strips any earlier markers from
`session["history"]` (in place, under the existing history_lock) before
appending the new one, so the live payload carries exactly one marker. This is
the payload re-sent each turn, and it's self-healing across resumes: whatever
markers a history reload brings back, the next switch collapses to one.

A stable `_MODEL_SWITCH_MARKER_PREFIX` constant is shared by the builder and
the `_is_model_switch_marker` predicate so the two can't drift; the marker
string itself is byte-for-byte unchanged. Existing behavior (role='user' per
the single db.append_message persistence) is preserved.

Scope note: this dedups the in-memory history (the per-turn cost the issue
quantifies). The DB still persists one row per switch for audit/resume; I did
NOT soft-archive prior marker rows because `active=0, compacted=0` is the
repo's rewind/undo sentinel (hermes_state.py get_messages docs), so
deactivating a marker naively would make it look rewound — a follow-up that
needs its own archive semantics.

Tests (tests/tui_gateway/test_model_switch_marker_role.py, +5): second switch
replaces (not stacks) the marker; the issue's exact 5-MoA-switch sequence
leaves one marker naming the last model; real conversation turns are preserved
in order; a stale marker sitting between turns is stripped; history_version
increments once per switch. `pytest test_model_switch_marker_role.py` → 13
passed (8 existing unchanged). Authored on Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-01 14:20:38 +05:30
kshitij c4e8613bba Merge pull request #76031 from kshitijk4poor/chore/contributor-email-rodboev
chore: add contributor email mapping for @rodboev
2026-08-01 14:20:17 +05:30
BB-light e9d52d2bda fix(caching): honor prompt_caching.cache_ttl disable in config
Setting prompt_caching.cache_ttl to a falsy value (false, null, off,
disabled, no, none) now fully disables prompt caching instead of
being silently ignored.

The disable propagates through anthropic_prompt_cache_policy() (early
return when _cache_disabled flag is set) and restore_primary_runtime()
(override after snapshot restore), so it survives /model switches and
fallback re-derivation — the gap that caused #56105 to be reverted in
#56126.

Salvage of #33555 by @BB-light, with model-switch/fallback survival
gap fixed on top.

Co-authored-by: BB-light <BB-light@users.noreply.github.com>
2026-08-01 14:14:15 +05:30
kshitijk4poor 7d54cb2672 chore: add contributor email mapping for @rodboev 2026-08-01 14:05:29 +05:30
kshitijk4poor 40e0e7ad56 docs(acp): correct late-refresh docstring — post-first-turn tools land via between-turns refresh
The claim 'late tools then require an explicit /reload-mcp' was false on
current main: the between-turns prologue refresh (agent/turn_context.py)
picks up late-connecting servers cache-safely at every turn boundary,
and ACP has no /reload-mcp. The daemon's real marginal value is tool-list
freshness in the [session created -> first message] window.
2026-08-01 13:27:58 +05:30
kshitijk4poor 23c13589dd fix(acp): serialize late MCP refresh with turn start + self-sufficient agent-build wait
Review follow-ups on the #32811 salvage:
- Hold state.runtime_lock and bail on is_running so the pre-first-turn
  guard can't race the first prompt dispatch (a refresh publishing
  mid-turn would swap tools= and break the just-created cache prefix).
  Regression test mutation-checked (guard removed -> test fails).
- In-memory-only session lookup in the daemon: get_session() falls
  through to a DB restore that builds a whole new AIAgent just to
  decide no-op (TUI equivalent also checks its in-memory dict only).
- Pass quiet_mode=True explicitly, matching the TUI/gateway callers.
- Use ensure_mcp_discovery_before_agent_build() (landed on main after
  the PR) instead of bare wait_for_mcp_discovery() so the ACP agent
  build is self-sufficient and gets the retry-after-zero-connected
  allowance, matching CLI/one-shot construction sites.
2026-08-01 13:27:58 +05:30
Sergey Prontsevich 640de6562e fix(acp): add bounded wait + late-refresh for configured MCP servers
ACP entry.py fires MCP discovery in a background daemon thread, but
_make_agent snapshots tools once at build and never re-reads the registry.
Unlike CLI/TUI, ACP had no bounded wait before the snapshot and no
late-refresh for configured (config.yaml) MCP servers — a reachable-but-
slow server that finished after agent build was invisible for the whole
session.

Changes:
- acp_adapter/session.py (_make_agent): call wait_for_mcp_discovery()
  before AIAgent construction, bounded by mcp_discovery_timeout (default
  ~1.5s). A dead server can't block; servers that miss the bound are
  picked up by the late-refresh below.
- acp_adapter/server.py (_schedule_mcp_late_refresh): new method on
  HermesACPAgent — if discovery is still in flight after session creation,
  spawns an off-critical-path daemon that joins it (bounded 30s), then
  rebuilds the tool snapshot via the shared refresh_agent_mcp_tools helper.
  Cache-safe: only runs pre-first-turn (_user_turn_count/_api_call_count
  both 0); once the user has sent a message the snapshot is frozen, exactly
  as TUI PR #48403 does.
- Called from new_session, load_session, resume_session.
- Mirrors the TUI pattern (tui_gateway _schedule_mcp_late_refresh, PR
  #48403) and the CLI pattern (get_tool_definitions → wait_for_mcp_discovery).

Tests:
- Replace the AST-based test (source-text inspection) with three
  behavioral regression tests in tests/acp_adapter/test_acp_mcp_discovery.py:
  1. Blocked discovery does not block startup (non-blocking contract)
  2. Delayed discovery lands tools via late-refresh (pre-first-turn)
  3. Late-refresh is cache-safe: skips rebuild after first turn

Addresses teknium1 review on PR #32811.
2026-08-01 13:27:58 +05:30
Sergey Prontsevich 89f0b63da4 perf(mcp): non-blocking startup via background MCP discovery + TUI fast path
Fire-and-forget MCP server connections on a daemon thread so the
gateway / CLI / ACP process becomes interactive immediately instead
of blocking on slow remote MCP servers (HTTP timeouts, sluggish
stdio boot).  Previously `hermes --tui` waited 2-5 s after the splash
screen before rendering the UI while `discover_mcp_tools()` ran
synchronously on the critical path.

Changes:
- tools/mcp_tool.py: add `discover_mcp_tools_background()` — thin
  wrapper that spawns `discover_mcp_tools()` on a named daemon thread
- tui_gateway/entry.py: call `discover_mcp_tools_background()` before
  sending gateway.ready (replaces inline call that blocked the JSON-RPC
  pipe for the TUI Ink app)
- hermes_cli/main.py:
  - skip `\_prepare_agent_startup()` for TUI path — plugins, MCP, and
    shell hooks are only needed by the CLI agent loop; the TUI's
    gateway subprocess discovers them independently (~370 ms saved)
  - fast-path in `\_make_tui_argv()`: when `dist/entry.js` exists and
    is fresh, skip npm install / rebuild checks entirely (~350 ms saved)
- cli.py (`\_prepare_deferred_agent_startup`): same background pattern
  for deferred startup (Termux interactive CLI)
- acp_adapter/entry.py: same pattern so ACP server launches asyncio
  immediately while MCP connects in parallel

Result:
- TUI Python wrapper: ~730 ms → ~80 ms (9× faster)
- gateway.ready: ~2700 ms → ~400 ms (7× faster)
- Total TUI cold start: ~3400 ms → ~480 ms

Related: #29726, #29184, #19326 (closed stale)

Closes #29726
2026-08-01 13:27:58 +05:30
brooklyn! 9b1d8341b2 Merge pull request #75988 from NousResearch/bb/cwd-focus-and-display
Session workspace tracks focus — and paints as ~/…
2026-08-01 02:53:44 -05:00
brooklyn! 1a25214364 Merge pull request #75998 from NousResearch/bb/pane-size-remember
fix(desktop): keep ⌘G/files rails at their default size
2026-08-01 02:52:43 -05:00
kshitij 3d5f3967d5 Merge pull request #75984 from kshitijk4poor/chore/contributor-emails-prontsevich
chore: add contributor email mappings for @Prontsevich
2026-08-01 13:20:30 +05:30
Brooklyn Nicholson 5657c4a541 fix(desktop): sort display-path import for eslint 2026-08-01 02:46:40 -05:00
Brooklyn Nicholson 47d7b7fb1d fix(desktop): keep review/files rails at their declared size
All-fixed splits used to promote the last track to flex-grow and drop its
max clamp. Review and files both declare maxWidth, so ⌘G/⌘J ballooned them
and sash overrides only set a basis that grow still expanded past.
2026-08-01 02:45:47 -05:00
HeLLGURD d109138ef5 fix(mcp): avoid replaying historical events on startup (#13414)
EventBridge initialized each session's last_seen timestamp to 0.0, so
the first poll after 'hermes mcp serve' starts treated every saved
user/assistant message in state.db as a fresh events_poll event.

The fix establishes a per-session timestamp baseline on startup via
_establish_baseline(), recording the latest existing message timestamp
without emitting events. Only messages written after the baseline are
delivered on subsequent polls.

Also hoists _ts_float to module-level (needed by _establish_baseline)
and adds ImportError fallbacks for hermes_constants imports so the
bridge works in environments where the module isn't on the path.

Salvage of #13414 by @afurm, re-applied by @HeLLGURD in #41239.

Co-authored-by: afurm <afurm@users.noreply.github.com>
2026-08-01 13:13:30 +05:30
Brooklyn Nicholson 883076ccd7 feat(desktop): ⌘N/⌘T keep the focused session's project
resolveNewSessionCwd now inherits the focused chat's workspace when you
aren't drilled into a sidebar project, so a new tab or draft stays in
the same repo as the chat you were looking at.
2026-08-01 02:32:28 -05:00
Brooklyn Nicholson 3707741d9f fix(desktop): statusbar cwd follows the focused session
Workspace indicator was stuck on the primary $currentCwd while timers
and context already tracked focus. Resolve from the focused runtime
slice, then the stored session row, with a mid-switch ownership gate.
Path tips across chrome use displayPath (~/).
2026-08-01 02:32:28 -05:00
Brooklyn Nicholson 5b4c57a7ff feat(desktop): shared path display collapses home to ~
One paint helper for UI chrome: /Users/x/y → ~/y (also /home and
C:\Users). Copy/reveal still use the real absolute path.
2026-08-01 02:32:28 -05:00
brooklyn! 57b1eb8c4d Merge pull request #75975 from NousResearch/bb/terminal-session-link
Link terminal tabs to the session you're working in
2026-08-01 02:22:30 -05:00
brooklyn! bfc014e3a8 Merge pull request #75966 from NousResearch/bb/dither-tail-only
fix(desktop): thinking indicator can no longer appear mid-transcript
2026-08-01 02:18:12 -05:00
Brooklyn Nicholson c450fb931d feat(desktop): switching sessions re-selects the terminal tab in its cwd
A $currentCwd listener in the terminal store picks the user tab whose
live shell cwd (restoreCwd, falling back to launch dir) matches the
session's workspace. Selection only: no tab is created, closed, or
revealed; detached sessions and unmatched cwds leave the rail alone,
and an already-matching active tab keeps focus.
2026-08-01 02:15:02 -05:00
Brooklyn Nicholson eb545ddea9 feat(desktop): ⌘-click closes a terminal rail tab
Same gesture the pane tabs already carry — isMetaClose hoists from
pane-tab.tsx into lib/middle-click.ts beside its sibling middle-click
gesture, so the two surfaces share one predicate instead of drifting.
2026-08-01 02:15:02 -05:00
Brooklyn Nicholson ba75633334 fix(desktop): never show the thinking indicator anywhere but the thread tail
A turn that ended without its message.complete (turn crash, reconnect gap,
steer race) left its streaming bubble pending:true forever. The next user
message then landed after it, stranding a live dither indicator
mid-transcript.

Three layers:
- session.info running=false (the agent loop's finally-block signal, the
  only settle edge those paths still emit) now finalizes the streaming
  bubble via the same math as Stop.
- A fresh submit settles any leftover pending bubble before appending the
  new user message, and drops a stale streamId so the new turn seeds fresh.
- AssistantMessage renders the loading/stall indicator only on the thread's
  last message, so no upstream state bug can ever paint one mid-transcript.
2026-08-01 02:10:56 -05:00
kshitij 151e72a5fc refactor: simplify pairing check return and drop over-defensive getattr
Follow-up cleanup from /simplify-code review:
- Replace 'if X: return True / return False' with 'return X'
- Replace 'getattr(source, "chat_type", None) or ""' with 'source.chat_type'
  (SessionSource.chat_type is a non-optional str field)
2026-08-01 12:38:38 +05:30
xxxigm 29b3adf902 test(telegram): cover allowlist + unauthorized_dm_behavior pair pass-through
Guard the early-auth pairing gap: unknown DMs must reach the gateway when
pairing is the effective unauthorized-DM behavior, while ignore mode and
unauthorized group senders stay rejected.
2026-08-01 12:38:38 +05:30
xxxigm dae4cf6bb6 fix(telegram): let pairing-bound DMs past early auth with allowlist
The #40863 intake prefilter rejected unauthorized DMs whenever an allowlist
existed, so gateway pairing never ran even when the operator set
telegram.unauthorized_dm_behavior: pair (which must win over the #9337
allowlist silence default). Pass those DMs through; groups stay blocked.
2026-08-01 12:38:38 +05:30
kshitijk4poor e5ba319a5c chore: add contributor email mappings for @Prontsevich 2026-08-01 12:37:17 +05:30
brooklyn! 18627ff009 Merge pull request #75931 from NousResearch/bb/tui-slash-priority
The TUI slash menu leads with the skills you actually use
2026-08-01 02:01:44 -05:00
kshitij 3572d4bca1 fix(mcp): ensure MCP discovery completes before agent build in non-interactive sessions
Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes #38448, #51316, #37013, #68137
Composite salvage of #60017 (chrishart0), #51322 (Bartok9), #38620 (buptwz),
#43544 (halonke), #36882 (vanhoof).
2026-08-01 12:27:31 +05:30