Commit Graph

2783 Commits

Author SHA1 Message Date
686f6c61 bf7c716648 fix(agent): rebind pool entry id after env credential refresh
Per-turn .env adoption could rewrite agent.api_key while leaving
_credential_pool_entry_id on a previously rotated fallback. The next 429
then marked the healthy fallback exhausted via credential_id precedence
(#79156).

- Sync pool entry id after a successful env credential refresh
- First look does not stomp a pool-rotated key with the env primary
- mark_exhausted_and_rotate prefers api_key_hint when it disagrees with
  credential_id

Fixes #79156
2026-08-08 19:17:02 -07:00
joaomarcos 628372de46 fix(otlp): span exporter now inherits configured resource_attributes
_resource_attributes() in otlp_exporter.py built its own hardcoded
resource dict (service.name/instance.id/telemetry.scope only) instead
of reusing gateway_health_export.py's _runtime_resource_attributes(),
which already applies the resource_attributes allowlist from config.
Result: operator-configured attributes like deployment.environment.name
reached metrics and diagnostic logs but never spans.

Span resource building now delegates to the same
_runtime_resource_attributes() helper metrics/logs already use,
removing the duplicate implementation instead of patching it in place.
2026-08-08 18:11:02 -07:00
Teknium 212e84176d fix(compression): charge stale thinking to the tail budget only on the newest assistant turn (#73624)
Generic thinking fields (reasoning / reasoning_content + the
reasoning_details text charge) are replayed for at most the NEWEST
assistant turn on every transport: Anthropic strips all-but-newest at
convert time, Bedrock Converse never replays thinking, and strict
chat-completions providers reject or one-space-pad the field. The tail
budget walks charged them on every message anyway, spending 19-24% of
the budget (per the issue's 1,025-message measurement) on bytes that
provably never reach the wire — so the tail cut landed early and each
compaction discarded more real transcript than configured.

_estimate_msg_budget_tokens now partitions the replay keys:

* _ALWAYS_REPLAYED_BUDGET_KEYS (codex_reasoning_items,
  codex_message_items) — charged unconditionally. These ride the wire
  on every retained turn (#55572), and codex_reasoning_items now also
  carries native server-side compaction checkpoints (#81747).
* _NEWEST_TURN_ONLY_BUDGET_KEYS (reasoning, reasoning_content) + the
  reasoning_details text charge — charged only for the newest assistant
  turn via charge_stale_thinking, resolved by the three budget walks
  (tail cut, raw-budget re-walk, proactive-prune boundary).

Default stays the conservative full charge for callers without
turn-position context. A partition invariant test pins that any future
_REPLAY_BUDGET_KEYS entry must be classified into exactly one class.

Direction credit: #73669 (@x7peeps) and #73730 (@webtecnica) both
attacked this; the keep_open reviews asked for provider/API-mode-aware
accounting that keeps Codex carriers charged — this implements that
shape.
2026-08-08 17:42:34 -07:00
Brooklyn Nicholson 86b50c6a29 fix(desktop): keep earlier HUD windows in scope for the turn
The HUD-mode note tells the model that an unqualified "this" means the app
behind the strip. It says nothing about the app that was behind it a minute
ago, and the user drags the strip from app to app mid-thought: parked over
Spotify, "pause that and play X here" is one request spanning two apps, and
only the second half has a window under it.

Those earlier windows are already in context as read_window_below results, so
the note only has to say they still count. Without it the latest window reads
as the only one and half the request is silently dropped.

No new tool names, so the existing gating tests cover it unchanged.
2026-08-08 19:17:30 -05:00
Brooklyn Nicholson f726090d48 feat(sessions): name a session the moment it starts
Titling fired on the first response, so a session sat unnamed for the whole
opening turn - p50 151s, p90 1212s across real sessions, because a turn is
tool calls, not one round-trip. A turn that failed or was interrupted never
got a title at all. Four surfaces each carried their own copy of the call.

Move it into the shared turn prologue and split it in two: a deterministic
title derived from the user's opening message, written inline before the
model runs, then one small-model call that upgrades it. The response is
constrained to a JSON object so there is no preamble to strip, and control
wrappers are stripped rather than refused, so a slash command titles as
what the user asked for instead of the command itself.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson e358eaf44a perf(sessions): resolve the titling model from the provider's live catalog
Titling ran on the user's main chat model, so a five-word title was billed
to a frontier reasoning model and inherited its latency. Pinning a cheap
model id instead just moves the problem: the hardcoded default was already
dead upstream and every call paid a 404 before the retry net caught it.

Match model FAMILIES against the provider's live /v1/models catalog,
preferring rolling '-latest' aliases where a provider publishes them, and
order the families by measured latency. Nothing to bump when a provider
ships a new mini/flash/haiku. Opt-in per task, so compression, vision, and
search keep 'auto means my chat model'.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson 5566379f57 fix(sessions): give titles provenance so they stop overwriting themselves
A session title had no notion of who set it, so two bugs followed. An
auto-generated title could clobber a name the user typed, and every
compression rotation renumbered the conversation it forked - one piece of
work reaching 'Smallville Map Architecture Plan #10' in the sidebar.

Titles now carry a source (derived < llm < user) enforced by one
compare-and-swap, so an automatic write can only ever replace a title of
strictly lower authority. Compression carries the name across unchanged.
Legacy NULL rows rank as user, so auto-titling only fills genuinely
empty titles on existing data.
2026-08-08 17:07:21 -05:00
Teknium 137960c9aa feat(media): opt-in upscale pass for image_generate and video_generate across FAL and Krea
The generated-media surface previously had almost no upscaler coverage:
only fal-ai/flux-2-pro chained Clarity Upscaler (hardcoded catalog
default), every other image model returned ~1MP output with no high-res
path, and video had no upscaler at all. Krea's API treats the enhancer
as a standard second pass; this brings the same shape to Hermes.

- image_generate: new optional 'upscale' boolean in the tool schema.
  Explicit true chains the backend upscaler on ANY model (including
  edits); explicit false disables flux-2-pro's automatic default;
  omitted keeps per-model catalog behavior. Response now reports
  'upscaled' so the agent knows which resolution it got.
- FAL image path: explicit flag overrides the catalog 'upscale' default
  (Clarity Upscaler, 2x). Failure falls back to the native image.
- Krea plugin: upscale=true chains Krea Enhance
  (/generate/enhance/krea/enhance, 2x, prompt-guided) through the same
  BYO/managed base URL + auth as generation, with a best-effort poll
  loop that never fails a successful generation.
- video_generate: new optional 'upscale' boolean; FAL video plugin
  chains ByteDance SeedVR2 (fal-ai/seedvr/upscale/video, 2x factor
  mode). Providers without upscalers ignore the kwarg per the ABC
  contract (documented in both ABCs).

Validation: targeted suites green (123 tests across 6 files, including
new coverage for override-wins/default-kept/failure-fallback on all
three paths); live E2E on direct FAL verified both chains end-to-end
(klein 9b + Clarity upscaled image; pixverse-v6 1s 360p + SeedVR2
upscaled video).
2026-08-08 14:49:28 -07:00
brooklyn! 51597c5e07
Merge pull request #82007 from NousResearch/bb/hud-surface-note
The agent knows when it's floating in HUD mode, and looks at the app underneath
2026-08-08 16:28:35 -05:00
Brooklyn Nicholson 0665cd4b5b style(hud): tighten the surface-note comments and test helper
Comment wording only, plus the desktop test's boolean parameter becomes
an 'app' | 'hud' union so the call site says which window it means.
2026-08-08 16:21:15 -05:00
Teknium e00965a7e8 fix(compression): correct prune boundary + exempt native compaction checkpoints
Two corrections on top of the #71077 base (the whole bug class):

1. Turn boundary = last USER message, not last assistant message. A Codex
   turn spans several assistant messages (assistant+tool_calls -> tool ->
   ... -> final assistant) whose reasoning items must replay together; the
   last-assistant boundary would strip reasoning mid-chain from the active
   turn (the gap flagged in PR #71077 review).

2. type="compaction" checkpoints (native server-side compaction, PR #81747)
   are exempt: they carry already-pruned history, not per-turn reasoning.
   Pruning filters items instead of popping the sidecar key.

Sibling site fixed in the same class: the Codex incomplete-continuation
dedup path blind-overwrote codex_reasoning_items on visually-duplicate
interim messages, which would drop the only copy of a checkpoint captured
on the earlier response. Extracted merge_interim_reasoning_items() into
agent/native_compaction.py; newer reasoning wins, prior checkpoints are
preserved unless the newer payload carries its own.
2026-08-08 14:09:41 -07:00
webtecnica adf9549cdd fix(compression): prune stale codex_reasoning_items during compaction (#71058) 2026-08-08 14:09:41 -07:00
Brooklyn Nicholson e24bac49fa feat(desktop): tell the agent when it is floating in HUD mode
In HUD mode Hermes is a strip over the app the user is actually working
in, so "what's under you?" or "look up the weather" is almost always
about that app — but the agent had no way to know it was floating, and
answered from its own browser and panes instead.

The desktop tags a HUD submit with `surface: 'hud'` and the gateway turns
that into a per-turn note pointing at read_window_below, and at carrying
the work out in the app underneath. It rides the model-bound message
beside the reaction and speech-interrupted notes rather than the system
prompt: one session can be driven from the app window on one turn and the
HUD on the next, and the system prompt has to stay byte-stable.

Every tool the note names is checked against the agent's own schema
first, so a session without computer_use or read_window_below is never
pointed at a tool it cannot call.
2026-08-08 15:37:41 -05:00
Teknium 9e6cfcda5a fix: finish the missing-encoding sweep — BOM-tolerant reads for user-edited stores
Complements the cherry-picked contributor fixes and closes out the
remaining sites of the 'missing explicit encoding' bug class, which is
now permanently gated by ruff PLW1514 (enabled repo-wide in
pyproject.toml and enforced by the blocking `ruff check .` step in
.github/workflows/lint.yml):

- tools/memory_tool.py: read MEMORY.md/USER.md via utf-8-sig so a
  Notepad BOM never glues U+FEFF onto the first entry (issue #10878,
  PR #10888 by @easyvibecoding — strict-decode contract of
  _read_raw_checked preserved rather than errors="replace", so
  undecodable files still refuse read-modify-write instead of being
  lossily rewritten). Regression tests included.
- tools/skills_tool.py: SKILL.md and skill file reads pinned to
  utf-8-sig + errors="replace" — deterministic across platforms instead
  of the locale fallback proposed in PR #51701 (superseded: falling back
  to cp1252/GBK makes the same skill render differently per host); .env
  reader aligned with the canonical utf-8-sig dialect in hermes_cli/config.py.
- agent/shell_hooks.py, hermes_cli/main.py, gateway/slash_commands.py:
  explicit utf-8 on the remaining fdopen/open text-mode sites flagged by
  the AlexFucuson9 sweep series (#56033 #56940 #65565 #66782 #66791).

Co-authored-by: easyvibecoding <easyvibecoding@users.noreply.github.com>
Co-authored-by: AlexFucuson9 <AlexFucuson9@users.noreply.github.com>
Co-authored-by: flyingdoubleg <wangzhe00zju@gmail.com>
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
2026-08-08 12:32:23 -07:00
solyanviktor-star 7fef76a6cf fix(auth): read .env as utf-8-sig in the dotenv-vs-shell detector
_remove_env_source() decides whether a credential var lives in ~/.hermes/.env
or the shell by scanning the .env with env_path.read_text(errors="replace") —
no encoding. read_text() with no encoding falls back to the system locale
(cp1252/GBK on Windows) and never strips a BOM.

The canonical .env readers in hermes_cli/config.py all use
encoding="utf-8-sig" precisely because 'users may edit .env in Notepad which
adds one' (a BOM), and doctor.py documents that .env is written as UTF-8
everywhere. This sibling reader diverged: on a Notepad-edited .env the BOM
prefixes the first line, so line.strip().startswith(f"{env_var}=") is False
for the first variable — the detector reports a .env-backed key as a phantom
shell export and prints a misleading 'still set in your shell environment'
hint on .

Match the canonical reader (utf-8-sig + errors=replace). Adds a regression
test with a BOM'd .env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 12:32:23 -07:00
Nolan 2fda6a384c fix(auth): cover remaining auth.json readers across modules
Follow-up to the auth.json UTF-8 read fix in this PR. A repo-wide scan for
the same bug class found three more callers that read ~/.hermes/auth.json
via Path.read_text() with no encoding — same Windows cp1252 hazard:

- agent/auxiliary_client.py _read_nous_auth: a non-ASCII byte raised
  UnicodeDecodeError, the broad except swallowed it, and Nous silently
  stopped being available as the auxiliary (vision/summarization) provider.
- tools/xai_http.py has_xai_credentials: same failure mode — xAI OAuth
  silently looked absent on Windows.
- hermes_cli/main.py is_setup_complete: same; has a config.yaml fallback so
  the impact is milder, but the read is still wrong.

All three now use read_text(encoding="utf-8-sig"), matching _save_auth_store's
write encoding. A repo-wide grep confirms there are no remaining
json.loads(...read_text()) reads of auth.json without an explicit encoding.

Tests: rewrote the Windows-encoding regression tests to actually exercise the
bug on POSIX too — a new windows_default_encoding fixture forces a no-encoding
read_text() to decode as cp1252 (the Windows default), and _write_utf8 now
emits real non-ASCII UTF-8 bytes (ensure_ascii=False) so the bytes actually
trip cp1252. Verified each test fails when its fix is reverted (including
the two new sibling-reader tests).
2026-08-08 12:32:23 -07:00
Teknium 566b5b16a9 fix(agent,gateway): class-level lone-surrogate chokepoints (#80366 #55143 #55309 #50959 #19819)
Own the surrogate-crash class at three chokepoints instead of leaf sites:

- finalize_turn scrubs final_response once where model text leaves the
  conversation loop — covers oneshot stdout (#80366), NIM/any-provider
  responses (#19819), and every delivery consumer of the turn result.
- _sanitize_gateway_final_response scrubs at the gateway chat-surface
  boundary — Telegram utf16_len (#55309) and Signal formatting (#55143)
  can no longer see a lone surrogate; raw-text surfaces keep passthrough.
- run_conversation walks the fully-built api_kwargs with
  _sanitize_structure_surrogates so tool descriptions (session_search,
  #50959) and every other request-body leaf are JSON-encodable before
  any provider sees them.

Regression tests pin all three chokepoints plus helper semantics.
Cherry-picked alongside #79240 (TheophilusChinomona) and #80374
(rainbowgore) whose commits precede this one with authorship preserved.
2026-08-08 12:31:19 -07:00
Teknium 5e1b50115f feat(compression): native OpenAI Responses server-side compaction for gpt-5.6
Opt-in via compression.codex_responses_native (default: false). When enabled,
gpt-5.6-family models on the direct OpenAI API (api.openai.com) or a ChatGPT
Codex subscription send context_management=[{type: compaction,
compact_threshold: N}] on Responses requests. OpenAI compacts server-side and
returns an encrypted compaction output item; Hermes captures it into the
existing codex_reasoning_items sidecar and replays it on later turns in place
of the pruned history — inheriting persistence, session replay, the
cross-issuer guard, and the encrypted-replay kill switch with zero new state.

Scope is deliberately hard-gated (agent/native_compaction.py, re-checked per
request): gpt-5.6 family only — gpt-5.1/5.2 fail server-side on the field
(HTTP 500 / stream stall, no structured rejection; live-verified) — and
direct OpenAI/Codex routes only; xAI, GitHub/Copilot, OpenRouter, relays,
and local servers never see the field.

Hermes' local compression stays armed as the fallback owner: the native
threshold is clamped ~8K tokens below the local trigger so the server
compacts first, and a structured provider rejection of context_management
disables native compaction for the session and retries without it
(one-shot guard in TurnRetryState).

Live-verified E2E on api.openai.com/gpt-5.6: server compaction fired at a
4K threshold, checkpoints captured and replayed, recall preserved across
3 turns; gpt-5.1 with the flag enabled stays clean (field never sent).

Direction credit: PR #76950 by @laryhorb explored native Responses
compaction; this is a minimal reimplementation on current main.
2026-08-08 11:24:45 -07:00
kshitij 0647bf9889 Revert "fix: rewire DCP context engine to current main architecture"
This reverts commit 9841a6c651.
2026-08-08 23:09:45 +05:30
kshitij 206f74baac Revert "feat: add DCP context engine"
This reverts commit d7072ab914.
2026-08-08 23:09:43 +05:30
kshitij 9841a6c651 fix: rewire DCP context engine to current main architecture
Fixes 13 issues found in PR #20774 review:

1. Wiring: engine selection moved from run_agent.py to agent/agent_init.py
   (where init_agent lives on current main). Transform hook moved from
   run_agent.py to agent/conversation_loop.py (where run_conversation lives).

2. Prompt caching: replace copy.deepcopy with copy-on-write (shallow list
   copy + clone only messages that are mutated). Use last_prompt_tokens
   from update_from_response instead of re-estimating tokens every call.
   System extension injection is idempotent (one-time cache break).

3. Signature mismatch: _message_signature renamed to _content_signature
   and now excludes tool_calls/tool_call_id from the hash. This prevents
   mismatches when _canonicalize_api_tool_calls re-serializes argument
   JSON with sort_keys=True on the API copy.

4. update_model: accepts api_mode parameter (required by agent_init.py).

5. Reconciled with select_context: transform_api_messages is a separate
   hook that runs AFTER select_context and sanitization, before
   prompt-cache marker placement. Both hooks coexist with clear ordering.

6. Dedup/purge: kept as DCP-specific strategies (different semantics from
   ContextCompressor._prune_old_tool_results — DCP deduplicates by
   tool+args signature, not by content hash).

7. Removed copy.deepcopy: replaced with shallow list copy + copy-on-write
   via _clone_if_needed. Only messages that are actually mutated get
   cloned.

8. Removed redundant _ensure_refs call: _match_api_messages_to_refs no
   longer calls _ensure_refs (the caller already called it).

9. _message_key still uses index (needed for positional ref assignment),
   but _content_signature is cached per id(msg) to avoid re-hashing.

10. _inject_nudge: only injects into user messages, never falls back to
    non-user messages (prevents role semantics violations).

11. Memory: _evict_inactive_blocks bounds blocks_by_id to
    _MAX_INACTIVE_BLOCKS (50) deactivated blocks.

12. Merged _range_tool_schema and _message_tool_schema into a single
    _compress_tool_schema. Merged _handle_range_compress and
    _handle_message_compress into _handle_compress.

13. Dropped DCP_CONTEXT_ENGINE_PR_SPEC.md (temporary file, not for tree).

Config defaults kept minimal in hermes_cli/config_defaults.py (only
the keys the engine actually reads, not the full DCP-compatible surface).

Closes #20717
2026-08-08 23:08:49 +05:30
Jack Maloney d7072ab914 feat: add DCP context engine
Cherry-picked from PR #20774 by @jmmaloney4 (jmmaloney4@gmail.com).
Original commits: 4420e0b0, 44247545, bac2955d.

DCP-style model-guided context engine behind context.engine: dcp.
Adds compress tool, outbound API-call transforms, automatic dedup/purge,
and DCP-compatible config surface.

Closes #20717
Co-Authored-By: Jack Maloney <jmmaloney4@gmail.com>
2026-08-08 23:08:49 +05:30
Brooklyn Nicholson 406501fd97 feat(agent): read_window_below tool — which OS window is underneath the desktop app
Desktop-gated (desktop_ui toolset) metadata-only window awareness: the agent
can ask which application window sits directly behind the Hermes window
(app, title, bounds — never pixels). Rides the same blocking bridge as
read_terminal: the gateway emits window.read.request and the renderer
answers window.read.respond.
2026-08-08 12:17:50 -05:00
webtecnica 464e7e4e5f fix(docker): read attached binary files in backend (#76577) 2026-08-08 05:44:18 -07:00
Teknium 5dc0fa3889 fix: post-merge audit follow-ups for #81138/#81139/#81141/#81148
Four fix-forwards from the adversarial post-merge audit of the Aug 7
unreviewed merge batch:

- estop (#81148): is_engaged() now fails SAFE (engaged) on stat errors;
  the gateway estop gate lets recognized slash commands and replies owned
  by in-flight work (update prompts, clarify, slash-confirm, tool
  approvals, running sessions) through instead of consuming them; new
  gateway /pause [reason|off] command gives messaging-only operators an
  in-band engage/resume path (busy_policy=dispatch so it works mid-run).
- cron monitor mode (#81138): execution-mode invariants (monitor x
  no_agent, monitor_script x monitor_url, no_agent-requires-script) now
  have ONE owner (_validate_job_mode_invariants) called from BOTH
  create_job and update_job, so the create-time invariant can no longer
  be silently violated through the update door.
- cron notepad (#81139): remove_job now clears the job's notepad rows
  (clear_notepad was dead code -> orphaned KV state forever); clear is
  best-effort and no-ops without creating notepad.db.
- delegation batch gate (#81141): template-marker regex narrowed to
  multi-word placeholder shapes only (<feature name>, {file_path}) so
  generics (Vec<T>), HTML tags, JSON snippets, glob braces and f-string
  style no longer reject legitimate batches; duplicate-goal rejection
  removed (best-of-N fan-outs are legitimate).
2026-08-08 05:21:09 -07:00
fangliquanflq fb4664f79d fix(learn): process large sources incrementally 2026-08-08 05:09:43 -07:00
fangliquanflq 57ca5995c6 fix(learn): extend existing skills during relearning 2026-08-08 05:09:43 -07:00
Teknium 1dee73400e Inspired by Cursor: fail-closed hook semantics + exit-code-2 blocking 2026-08-08 05:02:04 -07:00
Teknium 2e2fcc09ff Port from superagent-ai/grok-cli: directory-chain AGENTS.md loading 2026-08-08 04:31:45 -07:00
kshitij 973c14b57c refactor: fold simplify findings — 6th copy in update_cmd, drop dead wrapper + speculative kwarg, behavior-contract tests
- Migrate the missed 6th inline formatter (update_cmd.py backup-size
  display) to the shared helper.
- checkpoints._fmt_bytes: plain alias instead of a None-guard wrapper —
  every caller feeds ints from checkpoint_manager (all size fields
  initialize to 0), so the None path was dead defensive code.
- Drop the fallback= kwarg (zero production callers; '?' default is the
  real inherited contract and stays).
- curator_backup + context_references: call format_bytes directly (single
  internal call site each, zero external importers — alias was churn
  avoidance with nothing to avoid). backup/_format_size and
  doctor/_human_bytes keep their aliases (claw.py + tests pin the former;
  three call sites use the latter).
- Reshape the loop so the trailing TB return is reachable (no dead line).
- Tests: replace alias-identity assertions (ossified the delegation
  mechanism) with behavior-contract equality over a value sweep;
  mutation-checked red-green.
- update_cmd parity: byte-identical B-GB vs the old inline loop; gains
  the TB tier.
2026-08-08 15:10:35 +05:30
kshitij 7289898494 refactor: consolidate five duplicate byte formatters into hermes_cli.sizefmt
Five modules each carried a private near-identical human-readable byte
formatter (backup._format_size, checkpoints._fmt_bytes,
doctor._human_bytes, context_references._human_bytes,
curator_backup.format_size). Three of them silently topped out at GB and
rendered a 1 TiB value as '1024.0 GB'. All five now alias one shared
format_bytes in hermes_cli/sizefmt.py (sibling of timefmt.py, same
zero-dependency rationale), keeping each module's established local name
so no caller churns.

Deliberately NOT migrated (behavior differs on purpose):
- session_recovery._format_bytes: binary suffixes (KiB/MiB/GiB)
- qqbot chunked_upload.format_size: '100.0 B' one-decimal style, pinned
  by its protocol tests

Net -33 production LOC before the new module; parity verified over a
16-value corpus against all five verbatim originals (only divergence:
the TB tier fix). Contract tests mutation-checked red-green.
2026-08-08 15:10:35 +05:30
WolftacDigital 4af8fb2148 fix(ssl_guard): tolerate truststore SSLContext.get_ca_certs() NotImplementedError on Windows
On Windows, truststore.inject_into_ssl() replaces ssl.SSLContext with an
OS-trust-store-backed context whose get_ca_certs() raises NotImplementedError
(empty message). The ssl_guard's _validate_bundle_path() called get_ca_certs()
unguarded, crashing every fresh agent init with an opaque
'Failed to initialize OpenAI client:' error.

create_default_context(cafile=...) already validates that the bundle is
parseable, so we skip the post-load introspection rather than treat the
NotImplementedError as a failure.

Cherry-picked from PR #49945 with comment trimmed.

Co-authored-by: WolftacDigital <jonathan@wolftacdigital.com>
2026-08-08 15:10:24 +05:30
Erosika 4cc3ea01f6 fix(agent): separate continuation fragments so joined text does not glue
truncated_response_parts were joined with no separator at both the
ceiling exit and the success path, so a fragment ending mid-word ran
straight into the next one (#78577). insert a newline only when the
previous fragment ends non-whitespace and the next starts
non-whitespace, so existing separators are not doubled.
2026-08-08 14:56:38 +05:30
Erosika c8cf8bfdb6 fix(agent): strip length-continuation marks from outgoing api messages
the scaffolding marks are hermes bookkeeping. only the chat-completions
transport strips underscore keys, so anthropic and bedrock requests on
continuation attempts 2+ would send the marks to strict providers. pop
them in the central api_messages sanitization next to _thinking_prefill.

also pin that a mark reloaded from a mid-crash persist on a prior turn's
message is never deleted by a later turn's ceiling cleanup.
2026-08-08 14:56:38 +05:30
Erosika c5c040cb35 fix(agent): clean up the session tail when the continuation ceiling is exhausted
a turn that exhausts all 4 length-continuation attempts used to persist
its interim fragments and '[System: ... continue ...]' user nudges into
the session transcript. every later user turn replayed the unanswered
nudges, so the model resumed the oversized response, truncated again,
and re-exhausted the ceiling - wedging the session regardless of input.

at the ceiling exit, drop the fragment/nudge scaffolding from the turn's
tail and store one settled assistant turn carrying the stitched partial
text. the marks are cleared on continuation success and on the
content-filter rollback so cleanup can never delete fragments whose text
was already consumed.

also stop labeling a finish_reason='length' stub a network error: report
it as a truncation (stream ended before completion) and say the partial
response is kept when the ceiling is exhausted.
2026-08-08 14:56:38 +05:30
Erosika f0a3ef8bde fix(tools): harden live source checkout guard 2026-08-08 14:56:38 +05:30
Erosika a9f94022b0 fix(agent): bind finalize_turn at import time
The function-scoped import at the end of run_conversation loads
agent.turn_finalizer fresh from disk on the first turn that reaches it.
On a source/editable install whose checkout changed mid-session, that
pairs an old caller with a new callee at the exact seam where every turn's
work is persisted — the turn crashes on a signature mismatch after the
work is done. The lazy import was never cycle-forced: turn_finalizer
defers its own conversation_loop import.
2026-08-08 14:56:38 +05:30
kshitij d81f2f49ea refactor(compression): fold simplify findings — dedup floor constant, drift-guard test
- Wire the Pass-1 dedup floor (len < 200) to the shared _PRUNE_MIN_CHARS
  constant it was already documented as matching, and use the constant in
  the remaining test literal.
- Restructure the clarify 'resolved' computation (is_answer_shaped +
  sentinel check) instead of compute-then-flip.
- Add a live producer->recognizer drift guard: the REAL oneshot no-user
  callback's output must be recognized as a sentinel, so producer wording
  drift fails a test instead of silently reintroducing false attribution.
- Document the any()-poisoning semantic for multi-select sentinel lists.
2026-08-08 14:27:31 +05:30
kshitij 39056e8de4 fix(compression): filter clarify non-response sentinels; share prune floor constant
Follow-up to the salvaged #81244 commits:

- Timeout/no-user clarify callbacks (CLI timeout, gateway timeout and
  delivery failure, oneshot no-user) embed sentinel prose as
  user_response; quoting those as '[clarify] user responded: ...' would
  be false attribution. Route them to the generic summary path.
- Extract the shared _PRUNE_MIN_CHARS = 200 floor (prune default +
  proactive clamp) and cap the clarify summary at _PRUNE_MIN_CHARS - 1,
  removing the knife-edge equality the summary's survival depended on
  and keeping it out of the >=200-char dedup pass.
- Tests: 4 sentinel shapes + multi-select sentinel; mutation-checked.
2026-08-08 14:27:31 +05:30
Crypto Intern 3090e9e871 fix: reject forged clarify summaries 2026-08-08 14:27:31 +05:30
Crypto Intern 6433d5723f fix(compression): make clarify summaries UTF-8 safe 2026-08-08 14:27:31 +05:30
Crypto Intern d6511aecb6 fix(compression): preserve clarify responses 2026-08-08 14:27:31 +05:30
kshitij 98e96e1a60 refactor(agent): drop the run_agent classify_persistence_error delegating wrapper
Post-merge simplify finding on #81613: the wrapper's docstring claimed it
existed for 'existing callers', but every caller was introduced by the same
PR - there was never a pre-existing import path to preserve. All callers
(conversation_loop, tool_executor, run_agent's own flush handler, tests)
now import the canonical hermes_state.classify_persistence_error directly,
matching how is_disk_full_error is consumed. No behavior change; imports
stay lazy inside the exception handlers.
2026-08-08 14:25:12 +05:30
PRATHAMESH75 aed114a69b fix(agent): treat max-iteration nudge as synthetic during compaction
handle_max_iterations() appends its runtime summary request as a plain
role="user" row, which SessionDB persists verbatim. On later compaction the
synthetic-turn filters only recognized compaction summaries, continuation
rows, and todo snapshots, so the nudge could be selected as the latest
actionable user turn — becoming the task snapshot / auto-focus input and
getting summarized as "User asked: ...", demoting the real human task.

Metadata flags do not survive SessionDB projection (the reason the existing
markers are content-based), so recognition must key off stable content.
Extract the nudge into a shared MAX_ITERATIONS_SUMMARY_REQUEST constant and
teach _is_synthetic_compression_user_turn() to recognize it, mirroring the
continuation/todo markers. Every _is_actionable_user_turn call site already
pairs the synthetic guard, so the single recognizer change covers anchor
selection, auto-focus, and real-user-turn detection.

Fixes #78580
2026-08-08 14:24:39 +05:30
Victor Kyriazakos 2a9f5b3476 fix(agent): classify session-persistence failures so lock contention is not misdiagnosed as disk-full
An enterprise deployment hit sustained SQLite write-lock contention on a
shared multi-gigabyte state.db (gateway + CLI processes writing
concurrently). Turns correctly failed closed with
session_persistence_failed, but the only user-facing wording claimed the
disk was full and the gateway rendered a generic failure.

The fast-fail semantics are deliberate and unchanged. This adds a pure
classifier (locked / disk / unknown) applied where the SQLite error is
still visible, threads the cause through the turn-completion explainer,
and stamps a machine-readable failure_reason
(session_persistence_failed:<cause>) plus a guaranteed non-empty error on
the result for downstream surfaces. The cron scheduler's explainer-text
suppression now matches every cause variant so refined wording cannot
leak into scheduled-job deliveries.
2026-08-08 14:18:26 +05:30
Ken Weiner 367dda813c fix: print transform_llm_output appended content after CLI streaming
When the CLI streams a response token-by-token, it marks the response as
already displayed and skips re-printing after the tool loop. This means any
content appended by a transform_llm_output plugin fires after streaming — the
appended text is in the final response and stored in history, but never shown
to the user.

Fix by tracking the pre-transform response in finalize_turn() and including it
in the result dict as pre_transform_response. The CLI then checks whether the
response was transformed and, if so, prints only the appended suffix.

Previously the already_streamed branch was a no-op pass. Now it detects
post-stream plugin additions and outputs them without re-printing the streamed
body.
2026-08-08 14:06:30 +05:30
kshitij a4af262638 fix: revert except ImportError back to except Exception
load_config_readonly() can raise FileNotFoundError (deleted profile),
RuntimeError (managed mode), and PermissionError before any inner
try/except protects the caller. The except ImportError narrowing would
crash build_system_prompt_parts() — aborting agent startup — instead of
falling back to the base Telegram hint. The isinstance guards from the
second commit handle the TypeError case; the broad except Exception
handles the remaining failure surface.
2026-08-08 13:57:44 +05:30
bex 95520b812f fix(agent): fail open on malformed telegram extra config
Guard both extra lookups with isinstance(dict) before merging, so a
truthy non-mapping `extra` value (e.g. `extra: "true"`) degrades to the
base Telegram hint instead of raising TypeError and aborting
system-prompt construction. Keep the narrowed except ImportError.

Add an integration test exercising the real config path (HERMES_HOME +
gateway.platforms.telegram.extra.rich_messages) and a regression test
for the malformed-extra fail-open path. The integration test fails on
main and passes with the fix.
2026-08-08 13:57:44 +05:30
bex 9f582aca1d fix(agent): read Telegram rich_messages config from correct path
Commit b45a217e0 gated the TELEGRAM_RICH_MESSAGES_HINT extension behind
a config read at the top-level ``platforms.telegram.extra.rich_messages``
key, but the Telegram adapter reads the same setting from the canonical
``gateway.platforms.telegram.extra.rich_messages`` path.  When users set
the setting in the canonical location (the only one documented), the
lookup returned None and the extension never fired — the model degraded
pipe tables to bullet lists, task lists to plain dashes, and never
produced <details> blocks or block math.

Fix: merge both ``gateway.platforms.telegram.extra`` and the top-level
``platforms.telegram.extra`` with the same precedence the adapter uses
(top-level leaf wins), so config-wizard writes and dashboard-setup keys
are visible alongside the canonical gateway location.  Narrow the
except-guard to ImportError so real config-stack failures surface.
2026-08-08 13:57:44 +05:30
0xarkstar eaeba6474f feat(agent): add skip_background_review flag to AIAgent constructor
Phase 8 of the Hermes Agent token leak mitigation plan
(ralplan-hermes-token-leaks.md §3.9). Adds a boolean kwarg
`skip_background_review` (default False) to AIAgent.__init__ that
suppresses the end-of-turn _spawn_background_review fork.

Each background review fork instantiates a new AIAgent with its own
~15K input tokens + up to 8 LLM iterations, accumulating ~30K tokens
per event in the worst case. On cron sessions there is no
human-in-the-loop benefit from the review (no skill-creation pressure,
nobody curating MEMORY.md), so the cost is pure waste.

The end-of-turn guard now reads:

    if (final_response and not interrupted
            and not getattr(self, "skip_background_review", False)
            and (_should_review_memory or _should_review_skills)):

skip_memory=True already disables the memory-review trigger; this
flag is the explicit single-switch off for both review paths.

Defaults to False, so behavior is unchanged for gateway/CLI callers
that omit the kwarg.

Tests: 5 new unit tests in tests/agent/test_skip_background_review.py
covering the default value, flag persistence, the gate short-circuit,
the gate fall-through, and a source-text assertion that the cron
scheduler sets the flag to True (separate commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 00:07:14 +05:30