Only advertise finite watchdog deadlines that are still in the future, exercise the full MoA heartbeat path, and register the salvaged contributor attribution.
The dashboard console previously ran under a 'hosted' context that
blocked most commands (auth add, config set model.*, mcp add --command,
cron --script, ...) behind an allowlist + line-policy layer. With the
full Hermes CLI now built into the dashboard, that policy layer is
redundant gatekeeping: the console gets the same command surface
everywhere.
Removed:
- ConsoleContext/contexts plumbing on ConsoleCommand + engine
- EXPECTED_HOSTED_PATHS allowlist + _mark_hosted
- _enforce_hosted_line_policy + HOSTED_CONFIG_* allow/block tables
- _dashboard_console_context() and the context field on the ready frame
- hosted-context tests; context badge in HermesConsoleModal
Kept (mechanical, not policy): shell-syntax rejection, the
interactive/server command blocks (gateway, dashboard, mcp serve, ...),
mutating-command confirmations, output caps, and command timeouts.
Follow-ups on top of @xxxigm's salvaged bridge (#33294):
- Remove the now-dead narrow item/started-only mapper from #38835
(_codex_note_to_tool_progress) — the full bridge supersedes it and
keeps the same tool-name contract; its tests are repointed at the
bridge helpers.
- Preserve main's request_routing/approval-bypass wiring on the
CodexAppServerSession constructor (landed after the PR was filed).
- Gate agentMessage interim delivery on display.show_commentary so the
app-server runtime honors the same toggle as the codex_responses
commentary channel (tool progress is unaffected).
- Add json import (bridge helpers use json.dumps) and modernize the
wiring test's stub agent for main's usage-accounting attributes.
42 tests across five suites:
* ``TestCodexItemToToolName`` / ``TestCodexItemToArgs`` /
``TestCodexItemToPreview`` / ``TestCodexItemCompletionPayload`` —
pin the per-type mapping so the synthetic tool name + args the
UI sees match what ``CodexEventProjector`` writes into messages.
* ``TestStreamDeltaDispatch`` / ``TestToolProgressDispatch`` /
``TestAgentMessageInterimDispatch`` — drive each Codex
notification shape through the bridge and assert the right
agent callback fires with the right arguments (including the
duration / is_error / result kwargs the gateway renders).
* ``TestBridgeRobustness`` — defensive paths: non-dict
notifications, missing params, raising callbacks (must not
tear down the codex turn loop), and agents without callbacks
registered (cron / gateway-less contexts).
* ``TestBridgeWiredInRuntime`` — integration guard that
``run_codex_app_server_turn`` actually constructs the session
with ``on_event=<bridge>``, preventing a future refactor from
silently regressing live progress visibility again.
Pass ``on_event=make_codex_app_server_event_bridge(agent)`` when
spawning the per-session ``CodexAppServerSession``. The session has
always had a raw event hook but ``run_codex_app_server_turn`` never
supplied one, so Discord / Telegram / TUI users saw nothing while
codex was working — only the final answer landed.
Now each ``item/started`` for a tool-shaped item fires
``tool_progress_callback("tool.started", ...)``, ``item/completed``
fires the matching ``"tool.completed"`` with duration + result,
``item/agentMessage/delta`` flows through ``_fire_stream_delta`` and
each completed ``agentMessage`` surfaces through
``_emit_interim_assistant_message`` so the gateway's
``already_streamed`` dedupe keeps interim commentary in the channel
without duplicating text the stream already showed.
Adds ``make_codex_app_server_event_bridge(agent)`` plus four small
mapping helpers (``_codex_item_to_tool_name`` / ``_codex_item_to_args``
/ ``_codex_item_to_preview`` / ``_codex_item_completion_payload``)
that translate codex JSON-RPC ``item/*`` notifications into the
exact shape Hermes' gateway UI callbacks expect — tool names match
``CodexEventProjector`` so the progress bubbles and the projected
``tool_calls`` entries use the same identifiers.
No behaviour change yet: the next commit wires the bridge into
``run_codex_app_server_turn`` (#33200).
Review follow-up: the store-internals tests proved _current_cron_store()
resolves lazily, but not that the PUBLIC job I/O honors it. This exercises
save_jobs()/load_jobs() after a late env repoint and asserts the
import-time jobs.json stays byte-identical to a planted sentinel.
Review follow-up: tests that monkeypatch CRON_DIR/JOBS_FILE/OUTPUT_DIR (the
documented process-wide compatibility surface) were bypassed by the lazy
env fallback — 3 file-permission tests, the cross-process lock test, and
the heartbeat roundtrip regressed. _current_cron_store() now snapshots the
constants at import and honors any deliberate re-point of them ahead of
the env resolution, so the precedence is: use_cron_store() override >
patched constants > fresh HERMES_HOME > import defaults. Adds a test
pinning constants-beat-env; the late-env sentinel behavior is unchanged.
tests/cron: failure set byte-identical to unpatched main on this box
(the 5 regressions gone); 138 pass in the touched files.
Complements ec0227b43 (context-scoped cron store): the ContextVar override
is the right tool for deliberate cross-profile scoping, but with no
override active, _current_cron_store() returned the import-time constants —
so a HERMES_HOME set AFTER cron.jobs import (the filed incident: test
fixtures patching the env too late) still read/wrote the user's real
jobs.json. The fallback now resolves the active profile home fresh via
get_hermes_home() (context-local override, then env) and scopes the store
to it; when the home is unchanged since import, the exact module-level
constants are returned as before (zero change in the common path, and they
remain the documented compatibility surface). use_cron_store() still wins.
Three tests: late env repoint scopes the store; unchanged home returns the
import-time constants identically; an active use_cron_store() override
beats the env.
* feat(browser): store full snapshots on truncation; make eval denylist opt-in
Two harness fixes motivated by BU_Bench results where fixed-verb + lossy
observation cost Hermes heavily vs code-driven browser agents:
1. Snapshot truncation no longer loses content. When a snapshot exceeds
the 8000-char threshold, the complete accessibility tree is saved to
cache/web (same truncate-and-store pattern as web_extract) and the
truncated view / LLM summary includes the file path plus a ready-made
read_file call. Element refs beyond the cut are recoverable without
re-snapshotting. Stored copies are force-redacted and capped at 2MB;
content-hash filenames dedupe repeated snapshots of the same page.
2. The browser_console(expression=...) sensitive-primitive denylist is
now opt-in via browser.restrict_evaluate (default false). The
names-based denylist blocked legitimate DOM extraction — any selector
or expression containing 'fetch', 'cookie', 'input', etc. — which
crippled the agent's only programmatic page-inspection path. The
SSRF/private-URL egress guards in _browser_eval are independent of
this policy and remain always-on. browser.allow_unsafe_evaluate keeps
its meaning (bypass the denylist) for configs that already set it.
* test: update None-guard test for stored-snapshot pointer in _extract_relevant_content
test_normal_content_returned pinned the exact return value; the summary
now carries a pointer to the stored full snapshot. Assert the summary
passes through and the pointer is present instead.
* feat(browser): align snapshot threshold with web_extract's 15k char budget
SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching
web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract
truncate-and-store paths give the model the same per-page budget.
_truncate_snapshot's default max_chars now follows the constant.
Invariant test added; docs (en+zh) and CLI tip updated.
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).
- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)
Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
The openai-codex and xai-oauth branches of _refresh_entry duplicated the
lock-timeout computation and _auth_store_lock acquisition. Extract the
shared scaffolding: a combined provider guard, a dispatch to the
provider-specific sync helper, and a _single_use_refresh_lock_timeout()
helper. Each provider's distinct post-sync decision logic (codex
needs-refresh short-circuit vs xai token-equality adoption) is preserved
verbatim. Behavior parity verified by the credential pool suite (98
passed) and a direct timeout-helper probe for both providers.
Follow-up to salvaged PR #62285.
Keep each xAI OAuth auth-add login as an independent manual device-code pool entry and recognize xAI personal-team spending-limit 403 responses as billing exhaustion. Preserve the structured top-level error message so the failed credential is quarantined and the next healthy account is selected without attempting a pointless token refresh.
Route direct xAI HTTP consumers through the credential pool as well. Proactive and 401-reactive refreshes update the exact issuing manual entry, preserve validated xAI base URL overrides, and serialize single-use refresh-token rotation across concurrent pool instances.
Add a runtime_validator callback to generate_title() / auto_title_session()
/ maybe_auto_title(). Callers snapshot the session's model+provider when
spawning the background titler; the validator runs right before the LLM
request and skips it silently when the live runtime no longer matches —
so a stale title request can't reload a model that strict_single_load
already evicted after a user model switch. Fail-open: a raising validator
never disables titling.
Wired at all four call sites (cli, gateway, tui_gateway, acp_adapter).
Surgical reapply of PR #19137 (base was 8k+ commits stale; the original
patch predates the pinned-language prompts, the atomic-write helper, and
the moved TUI/ACP call sites). Original work by @Thatgfsj. Closes#19027.
Combines the two salvaged fixes so they compose instead of conflict:
_persist_session_title (#50575) now writes through set_auto_title_if_empty
(#51483) when the store provides it — the collision-dedup retry and the
manual-/title race protection apply together. Predicate failure (a manual
title landed while generation was in flight) returns None: nothing written,
no callback. Legacy stores without the atomic method keep the plain
set_session_title path, including the vanished-session RuntimeError.
Tests cover both store shapes plus the race-skip path; E2E verified against
a real SQLite SessionDB (collision -> 'Weekly Report #2', manual title
preserved, cron dedup, blank guard). AUTHOR_MAP entry for rasitakyol.
When the terminal's own 'disable mouse reporting' toggle (or an external
app / tmux) clears the DEC mouse modes, a mouse-only user is deadlocked:
every existing recovery trigger (resize, >5s stdin gap + keypress,
raw-mode bounce) needs stdin — and mouse reporting being off is exactly
why no stdin arrives. Users had to resize the window to scroll again.
Fix: a mouse-mode watchdog in App that DECRQM-probes mode 1000 every 2s
while tracking is expected on. If the terminal reports the mode RESET,
reassertTerminalModes() re-arms tracking — the same recovery a resize
performs, without the resize. Probes are skipped whenever a mouse/wheel
event arrived within the interval (tracking provably alive → zero query
chatter during normal use), never fired while paused for an editor
handoff or when /mouse off was chosen, and the watchdog permanently
disables itself on terminals that don't answer DECRQM (DA1-sentinel
resolution via the existing timeout-free TerminalQuerier).
Terminals whose toggle merely gates event delivery report SET, so an
active user toggle is never fought; terminals whose toggle clears the
modes report RESET after re-enable and recover within ~2s.
Follow-ups for the consolidated salvage:
- Memoize the config.yaml-derived timeout on the file's mtime_ns so the
rebuild-on-timeout-change check from PR #57437 costs one stat() per
get_honcho_client() call instead of a full YAML load on the hot path.
- hermes honcho status now displays the host-block-resolved
dialecticCadence (remnant from PR #63776, whose runtime fix landed
in #62290).
- AUTHOR_MAP entries for the salvaged contributor emails.
The Honcho client singleton cached the HTTP timeout at first build.
In long-lived processes (gateway, dashboard), changing the timeout
via config.yaml or HONCHO_TIMEOUT had no effect until restart.
Track the resolved timeout alongside the cached client and compare
on each get_honcho_client() call. When the timeout differs, reset
the singleton so the next call rebuilds with the new value.
Fixes#57347
Fixes Honcho client initialization for setup-generated configs that store
connection details in a named host block (e.g. "local"). Previously:
- base_url was only read from flat config root, not from host_block.
- resolve_active_host() ignored defaultHost and always used the Hermes
profile key ("hermes"), so the host block lookup returned {} and the
api_key was also lost.
FixesNousResearch/hermes-agent#61661
honcho_reasoning's minimal tier hard-caps Honcho's dialectic output at 250
tokens combined with the model's own hidden reasoning tokens. Confirmed via
direct honcho-api server logs that a multi-fact query ("summarize known
facts about this peer and communication preferences") run at
reasoning_level=minimal gets cut off mid chain-of-thought at exactly
output_tokens=250, before the model ever reaches a synthesized answer.
low/medium/high/max fall back to Honcho's much larger global dialectic
default and don't hit this cap. Since dialecticDynamic is on by default and
the calling model picks reasoning_level itself via this tool parameter, the
fix is to make the tradeoff explicit in the parameter description so the
model defaults to low unless the query is genuinely a single-fact lookup.
Schema-description-only change; the shared dialectic_max_chars truncation
cap in session.py's dialectic_query() is a separate fix (its own PR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verify that get_prefetch_context queries user context through the
assistant observer when _ai_observe_others is enabled, matching
the fix that routes _fetch_session_context through
_resolve_observer_target.
_fetch_session_context was using user_peer_id directly as observer
when calling _fetch_peer_context, bypassing _resolve_observer_target.
This caused honcho_context to return empty data because the observer
perspective was wrong (user instead of hermes/assistant).
Fixed by resolving observer via _resolve_observer_target(session, 'user'),
consistent with all other call sites (get_peer_card, honcho_search, etc.).
- Make the config imports lazy inside _auto_title_enabled(), matching the
existing _title_language() pattern (title_generator is imported from agent
code paths where a module-level hermes_cli import risks circularity).
- Check the enabled flag after the cheap first-exchange guard in
maybe_auto_title so config isn't read on every turn of a long session.
- Repoint the two new tests at the real import site.
- Document the key in cli-config.yaml.example and merge the enabled flag
into the existing title_generation block in configuration.md.
- AUTHOR_MAP entry for the contributor.
* fix: detect env-var-configured providers absent from PROVIDER_REGISTRY
is_provider_explicitly_configured() only checked PROVIDER_REGISTRY (a
manually-maintained dict) for env-var names. Providers that exist solely
in the models.dev catalog — e.g. openrouter — were never recognised as
explicitly configured, so they were filtered out of the desktop model
picker even when their API key was set in .env.
Add a fallback to get_provider() (which reads the models.dev catalog)
when PROVIDER_REGISTRY returns None. Both ProviderConfig and ProviderDef
expose .auth_type and .api_key_env_vars with the same shape.
* test: keep OpenRouter provider gate assertion behavioral
* chore(release): map salvaged OpenRouter contributor
---------
Co-authored-by: zzpigpinggai <zzpigpinggai@users.noreply.github.com>
Add a developer-guide page for running the Ink TUI and Electron desktop
app from a git worktree without a full npm install per checkout, via the
htui/hgui shell helpers that share node_modules from a canonical deps
checkout by symlink (falling back to a local npm ci when the lockfile
diverges). Registers it in the sidebar, cross-links from the TUI and git
-worktrees pages, and documents the previously-undocumented
HERMES_DESKTOP_PYTHON / HERMES_DESKTOP_DEV_SERVER env vars the desktop
backend reads.
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.
Measured on the two largest local sessions (996 and 1363 messages),
main-thread longtask totals per switch: warm 2450ms -> 557ms and
1158ms -> 194ms; first paint 1690ms -> 444ms. Harness:
scripts/profile-session-switch.mjs (same CDP family as
profile-real-stream.mjs).
- use-resize-observer: drop the synchronous initial callback and ride
the observer's spec-guaranteed first delivery instead (same frame,
after layout, before paint). The sync call ran while the commit's
layout was dirty, so every size read in a callback forced a full
reflow — with one instance per user bubble (measureClamp read
scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the
next bubble), the switch commit thrashed for over a second. Inside RO
timing the same reads are free. Composer metrics (2x
getBoundingClientRect + documentElement style writes) rides the same
fix.
- Same class, same fix at the remaining call sites profiling surfaced:
ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript)
now measure/pin via RO initial delivery; the tool-window and
thinking-preview pins drop their sync pin() call; the thread
timeline's initial active-tick compute joins its existing
scroll-time rAF batching so back-to-back transcript updates coalesce.
- thread/list: cut the render budget in the RENDER phase (state-from-
props adjustment) instead of the post-commit layout effect. The
effect-time cut was too late — on a warm switch React first built and
committed the full 300-part tree, then re-rendered at 60, then bumped
back to 300, so the expensive commit still happened (and on a cold
switch the bump rAF usually fired while the transcript was still
empty, so the prefetched messages rendered at full budget anyway).
The render-phase cut restarts the component before any child renders;
a second trigger handles the cold path where messages land later
under the same sessionKey.
- thread/list: backfill 60 -> 300 inside startTransition so the older
turns' markdown+shiki render is interruptible background work instead
of a synchronous freeze one frame after the switch paints. Functional
Math.max so an urgent "Show earlier" click can't be rebased back down.
- composer focus: skip the rAF/timeout focus retries when the element
is already focused — focus() runs the full focusing steps (forcing
layout) even on the active element, ~585ms per switch on a large
dirty DOM.
- Replace the tautological render-budget test (it re-declared the
constants locally and asserted 60 < 300) with behavior tests of the
now-exported buildGroups + firstVisibleGroupIndex.
Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
Post-hoc sprite removal is a losing battle (three inpainting strategies
failed QC on letter-edge overlap frames). The production answer: sprites
are BillboardComponent/SpriteComponent/ArrowComponent subobjects — set
bVisible:false via ObjectTools (remove_component fails on default
subobjects), swept scene-wide in one ProgrammaticToolset script
(148 actors, 13 sprites, one round-trip, verified).
New references/advanced-workflows.md covering the sophisticated-workflow
surface, each section exercised against a running editor:
- ProgrammaticToolset batching: full contract (get_execution_environment
gate, execute_tool fully-qualified names, JSON-string inputs,
returnValue unwrapping, allowed imports) + a verified worked example
(12-column colonnade, 36 components in one round-trip vs 37 serial calls)
- Blueprint DSL authoring loop, verified end-to-end: create -> list_graphs
-> get_graph_dsl_docs -> find_node_types per node -> write_graph_dsl ->
compile_blueprint -> spawn instance. Every node-ID gotcha hit live is
recorded (EventTick not Tick, Math|Rotator|MakeRotator, registry
categories vs doc categories, no (self) node)
- PIE session options schema (bSimulate/playMode/warmupSeconds/
startTransform, out-of-process downgrade behavior)
- Sequencer orientation: 140-tool surface mapped by capability group +
sibling keyframing/controlrig/conditions toolsets + minimal cinematic
skeleton
- LogsToolset self-debugging, AutomationTestToolset CI loop,
SemanticSearch, ConfigSettings, project AgentSkillToolset precedence
- Per-situation decision table
New pitfalls from this round's live failures: 10b (refPath-object vs
plain-string params; schema-in-error as tiebreaker), 10c (DSL node IDs
must come from find_node_types). SKILL.md: batching exception wired into
the operating loop, reference table row, description updated.