Sync upstream NousResearch/main → prod (20260530) - #40
Merged
Merged
Conversation
…esearch#34851) * fix(deps): declare setuptools in dev extra for packaging tests tests/test_packaging_metadata.py imports `from setuptools import find_packages` at module scope to validate package discovery against the live tree. setuptools was being picked up ambiently from the CI runner image, but recent ubuntu-latest images no longer ship it in the test venv, so collection fails with ModuleNotFoundError on every PR. Declare setuptools==82.0.1 in the dev optional-dependencies so `.[all,dev]` installs it explicitly rather than relying on the runner environment. * test(packaging): skip packaging-metadata tests when setuptools absent Belt-and-suspenders alongside declaring setuptools in [dev]: guard the module-level `from setuptools import find_packages` with pytest.importorskip so a runner missing setuptools SKIPS these checks instead of erroring out collection for the entire test shard. * chore(deps): sync uv.lock for setuptools dev dependency
…esearch#34530) (NousResearch#34845) * fix(auxiliary): stop capping output with max_tokens by default Auxiliary LLM calls (compression, titles, vision, etc.) no longer send max_tokens on the OpenAI-compatible chat-completions path. Most providers treat an omitted max_tokens as "use the model max", which is what we want; an explicit cap only risks truncation or a wire-format 400. This was surfaced by GitHub Copilot / GPT-5 (NousResearch#34530): those models reject max_tokens and require max_completion_tokens, so compression 400'd and fell back to a static context marker. Omitting the param sidesteps that quirk (and ZAI vision's error 1210) entirely. The Anthropic Messages wire (MiniMax + /anthropic endpoints) keeps max_tokens because it is a mandatory field there. * test(auxiliary): update temperature-retry assertions for omitted max_tokens The temperature-retry tests asserted retry_kwargs["max_tokens"] == 500 on an api.openai.com endpoint. Now that auxiliary calls omit max_tokens on OpenAI-compatible endpoints (NousResearch#34530), that key is absent. Assert it's absent in both first and retry kwargs and use model as the survives-the-retry witness.
The POSIX installer drops node/npm/npx symlinks in ~/.local/bin pointing
into $HERMES_HOME/node and prepends ~/.local/bin to PATH, shadowing an
existing nvm. Uninstall removed the hermes wrapper but left these behind,
so the user's default node/npm/npx stayed redirected after uninstall.
Add remove_node_symlinks() and call it from run_uninstall. It removes
~/.local/bin/{node,npm,npx} only when each is a symlink resolving into the
current Hermes home's node dir, so a link the user repointed at nvm or a
real binary is never touched. Handles dangling links too.
Closes NousResearch#34536
…ze up to here' (NousResearch#35048) Adds a user-chosen compression boundary to the existing /compress command. /compress here [N] summarizes everything except the most recent N exchanges (default 2), which are preserved verbatim — letting the user pick the compression boundary instead of relying on the automatic token-budget heuristic. Inspired by Claude Code's Rewind 'Summarize up to here' action (v2.1.139, Week 20, May 2026): https://code.claude.com/docs/en/whats-new/2026-w20 - hermes_cli/partial_compress.py: pure split/parse helpers + seam-alternation guard (shared by CLI and gateway). - cli.py / gateway/run.py: route 'here [N]' / '--keep N' to partial compression; compress only the head, re-append the verbatim tail through the seam guard. - Preserves message-flow role alternation (seam guard merges any illegal user->user / assistant->assistant adjacency). - Reuses the existing _compress_context session-rotation/lock machinery — no changes to the compression core. - Bare /compress (full) and /compress <focus> behavior unchanged. Tests: 12 helper unit tests + 5 CLI integration tests + E2E (interleaved tool-call transcript, degenerate/multimodal seams, real handler path).
…ousResearch#34839) handle_enter dispatches /steer and /model inline on the UI thread while the agent is running, calling buffer.reset() then returning. Unlike every other early-return branch in the handler, these two skipped event.app.invalidate(). process_command() prints through patch_stdout (scrolls output above the prompt without redrawing the input line), so the just-cleared input area could keep showing the submitted '/steer <text>' until an unrelated redraw fired — looking unsent and inviting an accidental re-submit. Add event.app.invalidate() after reset in both inline branches to match the sibling branches. AST regression test pins the invariant: every reset-then-return branch in handle_enter must invalidate first. Fixes NousResearch#34569
…test The rough-estimate mock supplied only 2 side_effect values but the conversation loop calls estimate_request_tokens_rough a third time for the post-response real-token estimate, exhausting the iterator. Use a callable side_effect that returns 125k once (to fire preflight) then sub-threshold values, independent of call count.
Hallucinated 'silence' tokens (*(silent)*, _silent_, the bare '.', '...',
'silent', no response/reply, the mute emoji) are emitted when a persona has
nothing actionable to say. In bot-to-bot channels the receiving bot mirrors
the token back, creating a tight loop that burns API tokens and can crash a
model with 'no content after all retries'. SOUL.md/prompt rules drift across
providers and have already failed in practice, so add a substrate-level guard.
_deliver_to_platform now drops a message whose finalized content is only a
silence-narration token, logs a WARNING with platform/chat_id/truncated
content, and returns {success: True, filtered: 'silence_narration',
delivered: False} instead of calling the adapter. Single chokepoint covers
every platform adapter; the regex is anchored start/end with a 64-char guard
so prose like 'Silence is golden — here is the plan...' or 'Silent install
completed' is never dropped. Local/file delivery is a separate path and is
left untouched. Opt out via gateway.filter_silence_narration: false or the
HERMES_FILTER_SILENCE_NARRATION env override (env wins when set).
Closes NousResearch#34616
Sharpen the label from 'Session usage (cumulative)' to 'Cumulative API tokens (re-sent each call)'. The number is real provider-reported usage summed across every API call in the session — not context size. In an agentic loop the same context is re-sent each iteration, so a one-hour tool-heavy session legitimately reaches tens of millions of tokens. The new label explains the magnitude so users stop reading it as a bug or as a total across all sessions.
…ousResearch#35079) * Inspired by Claude Code: /compress here [N] — boundary-aware 'summarize up to here' Adds a user-chosen compression boundary to the existing /compress command. /compress here [N] summarizes everything except the most recent N exchanges (default 2), which are preserved verbatim — letting the user pick the compression boundary instead of relying on the automatic token-budget heuristic. Inspired by Claude Code's Rewind 'Summarize up to here' action (v2.1.139, Week 20, May 2026): https://code.claude.com/docs/en/whats-new/2026-w20 - hermes_cli/partial_compress.py: pure split/parse helpers + seam-alternation guard (shared by CLI and gateway). - cli.py / gateway/run.py: route 'here [N]' / '--keep N' to partial compression; compress only the head, re-append the verbatim tail through the seam guard. - Preserves message-flow role alternation (seam guard merges any illegal user->user / assistant->assistant adjacency). - Reuses the existing _compress_context session-rotation/lock machinery — no changes to the compression core. - Bare /compress (full) and /compress <focus> behavior unchanged. Tests: 12 helper unit tests + 5 CLI integration tests + E2E (interleaved tool-call transcript, degenerate/multimodal seams, real handler path). * fix: keep CLI context display in sync with preflight token estimate The status bar reads compressor.last_prompt_tokens, which only updates from a successful API response. When loaded history is oversized but compression no-ops (e.g. the auxiliary summary model times out), no fresh usage arrives and the bar stays frozen at the old, smaller value while the preflight estimate reports a much larger number — looking permanently out of sync (reported: 74.4K display vs ~144,669 preflight). Seed last_prompt_tokens with the fresh preflight estimate (upward-only, so a real usage figure is never clobbered and a successful compression's downward correction still wins). Display-only; no behavioral change to compression, caching, or the agent loop.
When a turn ends abnormally after substantive tool calls (empty content after retries, a partial/truncated stream, exhausted retries, or an iteration/budget limit), the CLI/TUI response area was left blank or showed only a fragment (e.g. "The") with no consolidated reason. The internal turn_exit_reason values (empty_response_exhausted, partial_stream_recovery, etc.) were never surfaced to the user. Add a turn-completion explainer that mirrors the existing file-mutation verifier footer: at turn end, map an abnormal turn_exit_reason to a short, actionable message and either replace the bare "(empty)" sentinel or append the reason after a partial fragment. Normal text_response exits (e.g. a terse "Done.") stay quiet. Gated by display.turn_completion_explainer (default on) with HERMES_TURN_COMPLETION_EXPLAINER env override, matching the file-mutation verifier seam. Closes NousResearch#34452
PR NousResearch#34470 adds an explainer suffix to abnormal turn endings (e.g. max_iterations_reached) so users see why the response is short instead of receiving a bare/blank reply. test_tool_call_validation_accepts_dict_arguments runs the agent at max_iterations=3 which hits the explainer path; the existing strict-equality assertion (== "done") no longer matches once the suffix is appended. Switch the assertion to .startswith("done") so the test continues to verify that the models actual text survives intact while leaving the explainer suffix wording owned by conversation_loop (where it belongs). Test now passes (1 passed in 0.88s).
Follow-up to the salvaged NousResearch#34452 turn-completion explainer: - Register display.turn_completion_explainer: True in DEFAULT_CONFIG so the setting is discoverable, matching the file_mutation_verifier precedent. - Shorten the repeated footer prefix from 'Turn ended without a usable reply: ' to 'No reply: ' so the 10 reason variants don't all open with the same 8-word boilerplate. - Update the 7 assertions that referenced the old prefix.
…context window (NousResearch#35099) The compression threshold is threshold × context_length where context_length is the MAIN agent model's window, not the auxiliary/summary model's. On a 262,144-token model at the default 0.50 the threshold is 131,072 — close to a common 128K figure by coincidence of the percentage, which has led to confusion that the auxiliary model's context limit is the trigger. Add a note preempting that misreading and pointing to the separate summary-model-context constraint.
The salvaged contributor commit guarded only messages_fts. Current main also creates a second virtual table, messages_fts_trigram (CJK substring search), whose CREATE VIRTUAL TABLE ... USING fts5 still raised "no such module: fts5" on builds without FTS5 — re-crashing SessionDB init. Wrap the trigram setup with the same guard, and broaden the test's no-fts5 mock to fail BOTH tables so the regression test actually exercises a faithful no-FTS5 build.
uv's python-build-standalone distributions only gained FTS5 in mid-2025 (NousResearch#694). A stale interpreter already in uv's store — which `uv python find` reuses without checking — can lack it, leaving the supported install with a SQLite that can't create the FTS5 virtual tables hermes_state.py needs for full-text session search ("no such module: fts5"). check_python now probes the resolved interpreter for FTS5 and, if missing, reinstalls the latest patch for $PYTHON_VERSION (which has FTS5) and re-resolves. If an FTS5-capable Python still can't be obtained (offline, pinned env), it warns and continues — Hermes degrades gracefully and only disables session search. No bundled second SQLite, no user action.
When FTS5 is missing the warning now explains the likely cause (an unsupported / pip-managed Python whose bundled SQLite lacks FTS5) and links the supported install at hermes-agent.nousresearch.com, instead of just logging the raw error.
…naged_defaults (NousResearch#35109) apply_nous_managed_defaults() was adding image_gen and video_gen to the 'changed' return set without writing any config values. The caller (tools_command first_install flow) uses 'changed' to skip manual configuration, so these tools ended up in platform_toolsets but with no video_gen.provider, video_gen.use_gateway, or image_gen.use_gateway in config.yaml. At runtime the FAL plugin's is_available() returned False because there was no FAL_KEY and no use_gateway config — the tool never loaded despite being 'enabled' in the toolset list. For image_gen this was a latent bug masked by the gateway offer prompt (prompt_enable_tool_gateway) running earlier in the setup flow and writing image_gen.use_gateway=True via apply_gateway_defaults(). But if the user skipped the gateway offer, image_gen would silently break the same way. For video_gen (added in PR NousResearch#33259) the bug was always hit because the gateway offer ran before the user checked video_gen in the toolset checklist. Fix: write provider/use_gateway config values before adding to 'changed', matching the pattern used by web, tts, and browser.
Self-hosted Honcho setup had four sharp edges: - local/cloud URLs ending in /vN double-prefixed by the SDK (/v3/v3/... 404) - authenticated local servers had no setup prompt for a JWT/bearer token - profile-derived host keys could be dot-containing workspace IDs Honcho rejects - memory-provider config files with API keys written world-readable per umask This keeps existing behavior but makes those paths safer: - strip a trailing /vN version segment from any configured baseUrl before SDK init (the SDK's route builders always prepend their own version prefix); auth-skipping stays loopback-only - add an optional local JWT/bearer prompt in honcho setup, stored under hosts.<host>.apiKey - derive new profile host keys with underscores, still reading legacy hermes.<profile> blocks - write memory-provider config files atomically with 0600 via a shared utils.atomic_json_write(mode=) arg (honcho/hindsight/mem0/supermemory) - skip honcho.json parsing in gateway cache-busting unless Honcho is the active memory provider; memoize by honcho.json mtime when active - bust the gateway agent cache on memory.provider change - add a hermes memory setup <provider> one-liner so fresh installs can configure a named provider without the picker (the per-provider hermes <provider> subcommand only registers once that provider is active) Closes NousResearch#20688, NousResearch#29885, NousResearch#26459, NousResearch#30246, NousResearch#33382, NousResearch#32244. Co-authored-by: BROCCOLO1D
…ousResearch#35118) Starlette < 1.0.1 is affected by CVE-2026-48710 ("BadHost", CWE-444). The HTTP Host header was not validated before being used to rebuild `request.url`, so a malformed Host could make `request.url.path` desync from the raw ASGI path the router actually dispatched. Middleware and endpoints that apply path-based authorization off `request.url` (rather than `scope["path"]`) can therefore be bypassed. Hermes pulls Starlette transitively, never directly: - [web] -> fastapi==0.133.1 (starlette>=0.40.0, no upper bound) - [mcp] -> mcp==1.26.0 + sse-starlette (starlette>=0.27 / >=0.49.1) - [computer-use] -> mcp==1.26.0 - [dev] -> mcp==1.26.0 A fresh resolve landed starlette 0.52.1 — vulnerable. With no upper bound on the transitive specs, pip/uv could resolve any pre-1.0.1 release on a fresh install. Fix: pin starlette==1.0.1 directly in every extra that exposes a Starlette-backed server surface, regenerate uv.lock (only starlette moves: 0.52.1 -> 1.0.1, hash-verified), and mirror the pin in the lazy-install map (tools/lazy_deps.py `tool.dashboard`) so `hermes` on-demand dashboard installs can't re-resolve a vulnerable version. 1.0.1 is the advisory's named fix floor and the oldest patched release (more bake time than 1.1.0/1.2.0, which are days old); it satisfies every carrier constraint and our requires-python>=3.11. Scope note: this is a dependency-level fix complementing the application-layer Host-header validator added in NousResearch#34162 (`hermes_cli/web_server.py` `_is_accepted_host`). Defense in depth at both the framework and app layers. Guards: two invariant tests in tests/test_packaging_metadata.py assert every server-surface extra pins starlette and that pyproject + uv.lock both resolve >= the 1.0.1 CVE floor — a dropped pin or stale lock fails in CI instead of shipping the bypass. Closes NousResearch#35067
These tests asserted that hardcoded curated model lists/constants still
contained specific model strings (e.g. 'glm-5' in provider_model_ids('zai'),
exact context-length values per model key, PROVIDER_TO_MODELS_DEV entries).
They mirror a constant rather than exercise logic, so they only ever break
when models are added/retired and never catch a real bug.
Removed 22 such functions across 7 files (149 deletions, 0 additions).
Behavioral siblings are kept: live-catalog-wins, fallback ordering,
substring/longest-match resolution, normalization, credential discovery,
and probe-tier stepping all still tested.
The TUI already ships a rich /agents spawn-tree dashboard (live tree,
timeline, per-child tokens/cost/files/tools, kill/pause), but nothing
surfaced it — during delegation the transcript stayed quiet and users
had to already know to type /agents.
Drop a one-time transient activity hint ("subagents working · /agents
to watch live") the first time a turn starts delegating, matching the
existing "· /logs to inspect" house style. Guards keep it unobtrusive:
- fires at most once per turn (resets on message.start)
- silent when the /agents overlay is already open
- gated by display.tui_agents_nudge (default true)
Hooked on subagent.start, not subagent.spawn_requested: the delegate
progress callback in tools/delegate_tool.py only relays start/complete
to the gateway and drops spawn_requested, so start is the first
delegation event the TUI reliably receives. spawn_requested is wired
too for the future case, guarded once-per-turn.
Adds the display.tui_agents_nudge config default and gatewayTypes entry.
…nts-nudge feat(tui): nudge toward /agents dashboard when delegation starts
…ousResearch#35139) detect_install_method() returned "docker" for any container (is_container()), before the .git check. Both supported installs already self-identify via the .install_method stamp read first: the curl installer (scripts/install.sh) git-clones and stamps "git"; the published nousresearch/hermes-agent image stamps "docker" at boot via docker/stage2-hook.sh. An unsupported manual install dropped into a container has no stamp, so the bare container check hijacked it to "docker" and 'hermes update' bailed with the docker-pull guidance. Drop the redundant is_container() -> docker fallback. Unstamped installs now fall through to the .git/pip checks like any off-path install; both supported paths are unaffected because the stamp wins first. Fixes NousResearch#34397.
…uest (NousResearch#34436) The max-iterations summary path (`handle_max_iterations`) hand-builds its message list and calls `chat.completions.create()` directly, bypassing `ChatCompletionsTransport.convert_messages()`. It only popped ("reasoning", "finish_reason", "_thinking_prefill"), so `tool_name` (SQLite FTS bookkeeping), the `codex_*` reasoning carriers, and other internal `_`-prefixed scaffolding leaked to the wire. Strict OpenAI-compatible gateways (Fireworks-backed OpenCode Go, Mistral, Moonshot/Kimi) reject these with HTTP 400 "Extra inputs are not permitted, field: 'messages[N].tool_name'", so a long tool-using session that exhausts the iteration budget fails to summarise instead of returning the result. Mirror convert_messages() in this path: also drop tool_name, codex_reasoning_items, codex_message_items, and every `_`-prefixed key. Copy-on-write is already in place, so internal history keeps the fields for FTS / Codex-fallback. Adds a regression test to TestHandleMaxIterations asserting the summary request carries none of the schema-foreign keys (fails on main, passes here).
Allow non-loopback websocket peers when the dashboard is explicitly exposed with --host 0.0.0.0/:: and --insecure. This fixes the failure mode where /chat rendered over LAN but /api/ws and /api/events were rejected with HTTP 403, leaving the embedded TUI chat disconnected. Add regression coverage for the insecure public bind case in the dashboard websocket auth tests.
Adds _set_process_title() in hermes_cli/main.py, called first thing in main(). Tries setproctitle (optional) for a full ps-args rewrite, then falls back to ctypes prctl(PR_SET_NAME) on Linux / pthread_setname_np on macOS. No-op on Windows and on any failure. No new dependency: the setproctitle path is best-effort via ImportError guard. Fixes NousResearch#35108
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ousResearch#35278) Some Windows editors prepend an invisible UTF-8 BOM (U+FEFF) to text files. We had no awareness of it, so: read_file surfaced a phantom U+FEFF as the first character; patch matches against the true first line could miss; and a write/patch round-trip silently stripped the marker, changing the file's byte signature. Now: - read_file / read_file_raw strip a single leading BOM so the model never sees it (only on the first chunk — the marker lives at byte 0). - patch_replace strips the BOM before fuzzy-matching (so an exact first-line match works) and its post-write verification compares BOM-stripped content. - write_file restores the BOM when the original file had one and the new content doesn't, mirroring the existing line-ending preservation (detect on disk via a cheap `head -c 3` probe or reuse pre_content, re-prepend across the edit). Guards against double-BOM. Mid-content U+FEFF is left alone (it's data there, not a file marker). Tests: TestBomHandling (real LocalEnvironment) — read-strips, raw-read strips, write preserves, no-BOM-when-original-had-none, no-double-BOM, patch round-trip preserves, patch matches first line through a BOM, plus helper unit tests. 208 file-tool tests green.
…ead (NousResearch#35368) read_file's gutter used a fixed-width zero/space-padded prefix (" 1|content"). The padding is pure token overhead: measured with cl100k on real Hermes source, the padded gutter costs ~48% more tokens than bare content and ~16% more than a compact "<n>|content" gutter, because the leading spaces tokenize into extra tokens on every line. Switched the default to the compact "<n>|content" form. An A/B (Sonnet 4.6 via OpenRouter, 2 passes, 4-task battery, every claim verified against ground truth) showed: - padded : 4/4 PASS both passes - compact : 4/4 PASS both passes ← keeps line-referencing + patch - none : 3/4 PASS both passes ← dropping numbers entirely made the model hand-count lines and answer off-by-one (33 vs 34) So we keep the line numbers (the model genuinely uses them to reference lines) but drop the wasteful padding — capturing ~14% of the read-token cost with zero measured accuracy change. Dropping numbers entirely (the larger 33% saving) is rejected: it regresses line-referencing. patch/fuzzy_match never consumed the gutter (they match old_string text and compute char offsets internally), so editing is unaffected. No downstream parser keys on the fixed-width columns. HERMES_READ_GUTTER= padded restores the legacy format for anyone relying on alignment. Tests: updated the 3 format assertions to the compact gutter; added an env-override test for the legacy padded format. 209 file-tool tests green.
…earch#35378) * fix(file-tools): handle UTF-8 BOM in read_file / write_file / patch Some Windows editors prepend an invisible UTF-8 BOM (U+FEFF) to text files. We had no awareness of it, so: read_file surfaced a phantom U+FEFF as the first character; patch matches against the true first line could miss; and a write/patch round-trip silently stripped the marker, changing the file's byte signature. Now: - read_file / read_file_raw strip a single leading BOM so the model never sees it (only on the first chunk — the marker lives at byte 0). - patch_replace strips the BOM before fuzzy-matching (so an exact first-line match works) and its post-write verification compares BOM-stripped content. - write_file restores the BOM when the original file had one and the new content doesn't, mirroring the existing line-ending preservation (detect on disk via a cheap `head -c 3` probe or reuse pre_content, re-prepend across the edit). Guards against double-BOM. Mid-content U+FEFF is left alone (it's data there, not a file marker). Tests: TestBomHandling (real LocalEnvironment) — read-strips, raw-read strips, write preserves, no-BOM-when-original-had-none, no-double-BOM, patch round-trip preserves, patch matches first line through a BOM, plus helper unit tests. 208 file-tool tests green. * fix(kanban): respect mobile safe areas in task detail drawer The task detail drawer is a body-level z-60 fixed overlay using height:100vh starting at the viewport top. On mobile this puts the drawer header behind the dashboard's fixed top bar (min-h-14, z-40) and lets the bottom comment input sit under the browser's collapsing nav bar. - drawer: 100vh -> 100dvh (+ max-height:100dvh), 100vh kept as fallback - head: padding-top honors env(safe-area-inset-top); mobile (<1024px, matching the lg breakpoint where the fixed bar shows) clears the 3.5rem header - comment-row + body: bottom padding extended with env(safe-area-inset-bottom) so the bottom-most element clears the mobile browser chrome Mirrors the host shell idiom (100dvh + env(safe-area-inset-bottom) in web/), and web/index.html already sets viewport-fit=cover so the insets resolve. max()/calc() fallbacks leave desktop unchanged. Closes NousResearch#35324
…tus (NousResearch#35381) Empty model could reach the API on a recovery turn after stream_interrupt_abort, failing HTTP 400 "No models provided" with no recovery — the session went silent until the user manually re-sent (NousResearch#35314). - gateway/run.py: cache last-successfully-resolved model per session (+ a process-wide slot); when a fresh config read returns an empty model on a recovery turn, reuse the last-known-good instead of building model="". - run_agent.py + agent/conversation_loop.py: only emit "trying fallback..." status when a fallback chain actually exists, so the UI stops announcing a fallback that will never run (also NousResearch#17446). - tests: empty-model recovery + _has_pending_fallback gate.
…ate leak When _invoke_tool raises a BaseException (CancelledError, KeyboardInterrupt), the cleanup code at the end of _run_tool was bypassed because it sat outside the except block (which only catches Exception). ThreadPoolExecutor recycles thread IDs, so the leaked tid in _interrupted_threads poisons the next tool scheduled on that thread — it instantly aborts with 'Interrupted'. Move the discard + _set_interrupt(False) into a finally block so cleanup runs regardless of how the worker exits. Fixes NousResearch#35309
Follow-up on the NousResearch#35309 regression test: the trailing `with _lock: pass` asserted nothing. Replace it with a concrete assertion that _interrupted_threads is empty after the worker exits, directly verifying the leak the fix prevents.
…summary prefix
SUMMARY_PREFIX previously contained two contradictory directives:
1. "treat it as background reference, NOT as active instructions"
"Do NOT answer questions or fulfill requests mentioned in this summary"
"Respond ONLY to the latest user message that appears AFTER this summary"
2. "Your current task is identified in the '## Active Task' section of the
summary — resume exactly from there."
When the latest user message contradicted Active Task (e.g. 'stop the
i18n refactor', 'never mind, look at grafana instead'), models tended to
follow (2) anyway because 'resume exactly' is a strong, unambiguous
directive — leading to repeated re-surfacing of already-cancelled work
across turns, even after explicit 'stop'/'don't keep bringing that up'
messages from the user.
This change:
- Removes the conflicting 'resume exactly from Active Task' clause.
- Makes the precedence explicit: latest user message is the single source
of truth; it WINS on conflict; cancelled Active Task / In Progress /
Pending User Asks / Remaining Work must be discarded entirely (no
'wrap up the old task first').
- Names canonical reverse signals (stop, undo, roll back, never mind,
just verify, topic change) so the model recognizes them as cancellation
triggers, not background context.
- Updates the summarizer template instruction so the LLM doesn't
mechanically copy a cancelled task into Active Task on the next
compaction (it's instructed to copy the reverse signal verbatim).
- Preserves: REFERENCE ONLY framing, MEMORY.md/USER.md authority, and
the 'don't repeat work already reflected in session state' clause.
Adds tests/agent/test_summary_prefix_semantics.py to pin invariants so
the conflict can't regress.
Tested:
- All compaction tests pass: tests/agent/test_context_compressor.py,
tests/agent/test_context_compressor_summary_continuity.py,
tests/run_agent/test_413_compression.py,
tests/run_agent/test_compression_persistence.py,
tests/run_agent/test_compression_boundary_hook.py,
tests/cli/test_manual_compress.py — 117/117 passing.
- Tested on macOS.
…'None' The Active Task field in compression summaries is the single most important field for task continuity across context boundaries. The previous template described it narrowly as a 'task assignment' or 'request', which caused the summary LLM to write 'None' whenever the user's most recent input was a question, a decision request, or a discussion turn rather than an imperative command. The assistant on the other side of the compaction then treated the conversation as resolved and gave a generic recap instead of answering the still-open question. Expand the template guidance to cover: * explicit task assignments * questions awaiting an answer * decisions awaiting input (A vs B) * ongoing discussions where the assistant owes the next substantive reply Reserve 'None' for the rare case where the last exchange was fully resolved (e.g. user said 'thanks, that's all'). Also tighten the trailing CRITICAL instruction in the summary prompt so the LLM cannot fall back to the old 'no imperative command → None' heuristic. No behavioural code changes — template strings only. All 83 existing compressor tests pass.
…Research#26290+NousResearch#32787 (NousResearch#35344) A handoff persisted under an older SUMMARY_PREFIX can be inherited into a resumed lineage. _strip_summary_prefix only matched the current/legacy literal, so on re-compaction the old 'resume exactly from Active Task' directive stayed embedded in the body and kept hijacking replies to new, unrelated user messages. - Add _HISTORICAL_SUMMARY_PREFIXES (pre-NousResearch#35344 prefix) and strip/recognize them in _strip_summary_prefix + _is_context_summary_content so resumed stale handoffs are re-normalized to the current latest-message-wins prefix. - Reconcile the overlapping Active Task template edits from the salvaged NousResearch#26290 (reverse-signal cancellation) and NousResearch#32787 (capture open questions / decisions, don't write None too eagerly) — both intents kept. - Regression coverage in tests/agent/test_resume_stale_active_task.py. - AUTHOR_MAP entries for both salvaged contributors.
…r_console (NousResearch#35385) browser_console(expression="document.body") returned the cryptic CDP error "Object reference chain is too long" instead of a usable result. With returnByValue=true, Chrome deep-serializes the eval result; for a live DOM Node/NodeList/Window that serialization overruns CDP's recursion guard and fails the whole call with a protocol-level error (not a JS exception), which _browser_eval surfaced raw. - browser_supervisor.evaluate_runtime: on that specific error, retry once with returnByValue=false so Chrome returns the node's description string — the same graceful path already used for document.querySelector() results. - browser_tool._browser_eval (CLI subprocess fallback): the subprocess can't retry, so convert the reference-chain error into actionable guidance (extract a primitive / use JSON.stringify) instead of leaking it raw. No expression rewriting — normal evals (1+41 -> 42) are untouched.
Layer an exception guard on top of the empty-response fix so a crash inside the agent (e.g. OSError from prompt_toolkit/Vt100 when stdout is a non-TTY pipe, per NousResearch#30623) is surfaced on the real stderr with rc=1 instead of crashing past the redirect_stderr block. KeyboardInterrupt/SystemExit are re-raised so Ctrl-C and explicit exits still propagate. Also map briancl2 in scripts/release.py AUTHOR_MAP for the cherry-picked empty-response commit. Adapts the exception-guard approach from sweetcornna's PR NousResearch#33818. Co-authored-by: sweetcornna <96944678+ymylive@users.noreply.github.com>
NousResearch#35386) The merged 0.0.0.0/:: insecure-bind fix (NousResearch#35141) did not cover binding directly to a specific non-loopback address (e.g. a Tailscale/LAN IP via --host 100.64.0.10 --insecure). In that mode the dashboard HTML loaded but every WebSocket upgrade was rejected by the loopback-only peer guard, so /chat connected then silently received no data. Generalize _ws_client_is_allowed to lift the loopback-only peer gate for any explicit non-loopback bound host, not just the 0.0.0.0/:: wildcard. DNS-rebinding stays blocked: _ws_host_origin_is_allowed already requires the Host header to exactly match the bound interface for explicit binds, mirroring _is_accepted_host on the HTTP layer. Co-authored-by: pxdsgnco <14163800+pxdsgnco@users.noreply.github.com>
WhatsApp and WeChat (Weixin/iLink) both deliver messages individually without any client-side batching, so rapid multi-message bursts (forwarded batches, paste-splits, etc.) each trigger a separate agent invocation. This wastes tokens (redundant system prompts / context for each fragment) and degrades UX (the user receives reply fragments instead of a single coherent response). Both adapters now mirror the Telegram adapter's proven text-debounce pattern: - _text_batch_delay_seconds / _text_batch_split_delay_seconds (configurable via env vars) - _pending_text_batches dict for per-session aggregation - _enqueue_text_event() concatenates successive TEXT messages and resets the flush timer - _flush_text_batch() dispatches after the quiet period expires Configurable via env vars: HERMES_WHATSAPP_TEXT_BATCH_DELAY_SECONDS (default 5.0) HERMES_WHATSAPP_TEXT_BATCH_SPLIT_DELAY_SECONDS (default 10.0) HERMES_WEIXIN_TEXT_BATCH_DELAY_SECONDS (default 3.0) HERMES_WEIXIN_TEXT_BATCH_SPLIT_DELAY_SECONDS (default 5.0)
Convert the salvaged text-debounce delays from HERMES_* env vars to config.yaml (gateway.platforms.<name>.extra.text_batch_delay_seconds / text_batch_split_delay_seconds), per the '.env is for secrets only' policy. Adds a finite/non-negative guard so bad YAML values fall back to the defaults instead of crashing asyncio.sleep(). - whatsapp.py / weixin.py: read delays via _coerce_float_extra(config.extra) - update Weixin content-dedup regression test for the deferred dispatch path - add text-debounce coverage (whatsapp + weixin): defaults, config override, bad-value fallback, env-var-ignored, burst-collapse, lone-message - docs: WhatsApp + Weixin config keys
…lures (NousResearch#35387) The per-platform reconnect watcher auto-paused a platform after 10 consecutive reconnect failures, setting next_retry=inf and requiring a manual /platform resume to recover. But both pause sites only ever fire on *retryable* failures — non-retryable errors (bad auth) already drop out of the retry queue earlier. So a transient DNS outage that spanned the watcher's backoff window would silently park the bot forever, even after connectivity returned. The watcher's own docstring already promised 'retryable failures keep retrying at the backoff cap indefinitely' — the code contradicted it. Remove the auto-pause from both reconnect-failure branches. Retryable failures now retry at the 5-min backoff cap forever and self-heal once the network recovers. The circuit breaker (_pause_failed_platform / _resume_paused_platform) stays for manual /platform pause|resume. Fixes NousResearch#35284.
…xtract_local_files (NousResearch#34632) The MEDIA_TAG_CLEANUP_RE and extract_local_files path regex both used (?:~/|/) to anchor paths, which only matches Unix-style absolute and home-relative paths. Two additional _TOOL_MEDIA_RE patterns in run.py had the same limitation. Windows absolute paths (C:\Users\..., D:/...) were silently ignored, causing MEDIA directive delivery to fail. Add [A-Za-z]:[/\\] as a third anchor alternative in all four regex locations (base.py x2, run.py x2). Also update path separators in extract_local_files from / to [/\\] so it can traverse Windows directory trees. Revert accidental + quantifier in MEDIA_TAG_CLEANUP_RE lookahead that changed match-one to match-one-or-more (unrelated to fix). Fixes: NousResearch#34632
…ehavior test_windows_path_not_matched asserted the pre-fix POSIX-only behavior. The Windows drive-letter support now intentionally matches these paths, so replace it with parametrized positive cases plus a relative-path negative guard, mirroring tests/gateway/test_platform_base.py.
Tasks can now carry file attachments (PDFs, images, source docs) that workers read directly — closes the gap where source material had to be pasted as a path into the task body. - kanban_db: task_attachments table (additive), Attachment dataclass, add/list/get/delete accessors, attachments_root/task_attachments_dir path helpers (per-board, HERMES_KANBAN_ATTACHMENTS_ROOT override) - build_worker_context: surfaces each attachment's absolute path so the worker (full file/terminal tool access) reads it via read_file/pdftotext - dashboard API: POST/GET/DELETE attachment routes (multipart upload, 25MB cap, traversal-safe filenames, root-containment check on download) - dashboard UI: Attachments section in the task drawer — upload button, list with download, per-row remove - docs + tests (13 cases: DB accessors, REST round-trip, traversal rejection, collision suffixing, worker-context surfacing) Closes NousResearch#35338
…port resolved path (NousResearch#35399) Relative paths in write_file/patch could resolve against the agent PROCESS cwd instead of the terminal's working directory. In a git-worktree session with a stale TERMINAL_CWD='.' (a relative base), early edits silently landed in the MAIN checkout, verified there, and reported success — while the agent inspected the worktree and saw nothing, misreading it as the patch tool no-op'ing. - _resolve_base_dir(): resolution base is now ALWAYS absolute. A relative TERMINAL_CWD is anchored to the process cwd once, deterministically, instead of being left to resolve()-time cwd. Live terminal cwd stays authoritative. - write_file/patch pass the resolved absolute path to the shell FileOps layer so the tool layer and shell layer can't disagree about which file is edited. - Responses now report the absolute resolved_path and files_modified, so a wrong-cwd mismatch is visible on the first call. - _path_resolution_warning(): emits a _warning when a relative path resolves OUTSIDE the live terminal cwd (e.g. a worktree session writing into main). Validation: 11 new unit tests + 43 live E2E assertions (worktree routing, mid-session cd, V4A patches, divergence warning, absolute paths, consecutive patches); 466 existing file/path/terminal tests green.
…ousResearch#35441) Over SSH the OSC 11 background-color query round-trip routinely exceeds the 100ms read budget, so _query_osc11_background() gives up and the late reply lands after prompt_toolkit has grabbed the tty. prompt_toolkit then injects the OSC payload as typed text and reads its BEL terminator (\x07 = Ctrl+G) as a keystroke — Ctrl+G is the open-external-editor binding, dropping the user into vi with garbage and no obvious way out. - Skip the OSC 11 probe on remote sessions (SSH_CONNECTION/CLIENT/TTY); fall back to COLORFGBG / env hints / the dark default. - Restore the tty with TCSAFLUSH instead of TCSANOW so any partial/late reply is scrubbed from the input buffer before pt reads it.
…D_GUTTER (NousResearch#35532) The compact "<n>|content" gutter from NousResearch#35368 is now the sole behavior. Removes the HERMES_READ_GUTTER=padded escape hatch and its env lookup — no legacy fixed-width path to maintain. Padding was pure token overhead (~48% more tokens than bare content, ~16% more than compact) with no measured accuracy gain in the original A/B. - file_operations.py: drop env lookup + os import; gutter always f"{i}|{line}" - tests: drop the padded env-override test; compact assertions retained
# Conflicts: # scripts/release.py
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
18 |
unresolved-attribute |
9 |
invalid-assignment |
7 |
invalid-argument-type |
4 |
no-matching-overload |
1 |
First entries
tests/test_honcho_client_config.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/main.py:9042: [no-matching-overload] no-matching-overload: No overload of function `run` matches arguments
tests/hermes_cli/test_mcp_startup.py:151: [invalid-assignment] invalid-assignment: Object of type `() -> None` is not assignable to attribute `_ensure_tirith_security` of type `def _ensure_tirith_security(self) -> None`
tests/hermes_cli/test_uninstall_node_symlinks.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_nous_subscription.py:316: [invalid-argument-type] invalid-argument-type: Argument to function `apply_nous_managed_defaults` is incorrect: Expected `dict[str, object]`, found `dict[str, dict[str, str]]`
tests/tools/test_mcp_stability.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_run_tool_media_re.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_prompt_size.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_mcp_startup.py:150: [invalid-assignment] invalid-assignment: Object of type `() -> None` is not assignable to attribute `_install_tool_callbacks` of type `def _install_tool_callbacks(self) -> None`
tests/run_agent/test_turn_completion_explainer.py:54: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_use_prompt_caching` on type `AIAgent`
tests/plugins/memory/test_mem0_v2.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_mcp_startup.py:147: [invalid-assignment] invalid-assignment: Object of type `object` is not assignable to attribute `_session_db` of type `None | SessionDB`
tests/gateway/test_weixin.py:950: [invalid-assignment] invalid-assignment: Object of type `def _capture(event) -> CoroutineType[Any, Any, Unknown]` is not assignable to attribute `handle_message` of type `def handle_message(self, event: MessageEvent) -> CoroutineType[Any, Any, None]`
tests/run_agent/test_turn_completion_explainer.py:53: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_cached_system_prompt` on type `AIAgent`
tests/agent/test_set_runtime_main_custom_provider.py:6: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/platforms/whatsapp.py:1224: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
tests/run_agent/test_turn_completion_explainer.py:55: [unresolved-attribute] unresolved-attribute: Unresolved attribute `tool_delay` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:59: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_fallback_chain` on type `AIAgent`
tests/tools/test_vision_tools.py:985: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/tools/test_file_tools_cwd_resolution.py:21: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_mcp_startup.py:152: [invalid-assignment] invalid-assignment: Object of type `() -> Literal[True]` is not assignable to attribute `_ensure_runtime_credentials` of type `def _ensure_runtime_credentials(self) -> bool`
hermes_cli/main.py:84: [unresolved-import] unresolved-import: Cannot resolve imported module `setproctitle`
gateway/run.py:12673: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent._compress_context` is incorrect: Expected `str`, found `str | None`
tests/agent/test_set_runtime_main_custom_provider.py:221: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_provider_client` is incorrect: Expected `str`, found `None`
plugins/kanban/dashboard/plugin_api.py:50: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.responses`
... and 14 more
✅ Fixed issues (3):
| Rule | Count |
|---|---|
invalid-argument-type |
2 |
unresolved-attribute |
1 |
First entries
tests/hermes_cli/test_kanban_blocked_sticky.py:132: [unresolved-attribute] unresolved-attribute: Attribute `last_failure_error` is not defined on `None` in union `Task | None`
gateway/run.py:12578: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent._compress_context` is incorrect: Expected `str`, found `(LiteralString & ~Literal[""]) | None`
agent/auxiliary_client.py:3038: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_provider_client` is incorrect: Expected `str`, found `None | (Any & ~AlwaysFalsy)`
Unchanged: 4948 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge upstream
NousResearch/maininto prodmain(96 commits behind).Mirrors the canary sync already merged + soaked (canary PR #24, live on PB10).
Conflicts:
scripts/release.pyAUTHOR_MAP (class B, kept both sides + canary's redpiggy-cyber/fork-maintainer entries).Seams verified: venice/bankr/reference_images intact; build workflow preserved via
merge=ours(unchanged from prod origin); amd64-only.Notable upstream: Starlette CVE-2026-48710 pin (NousResearch#35118), atomic write_file/patch (NousResearch#35252), UTF-8 BOM handling (NousResearch#35278), compact read_file gutter (NousResearch#35532/NousResearch#35368), custom-provider aux routing (NousResearch#34530), gateway transient-failure resilience (NousResearch#35387), /compress here (NousResearch#35048).
CI must pass all
test (1..6)+ lint before merge.