Commit graph

7610 commits

Author SHA1 Message Date
kshitijk4poor
7332f207d7 test(compaction): initialize anti-thrash fixture state 2026-07-11 22:59:49 +05:30
Igor Ganapolsky
7f9485707d fix(compaction): judge the anti-thrash verdict on real usage, not in should_compress
Third correction, and the load-bearing one. The previous commit put the
"did compaction clear the threshold?" verdict inside should_compress(). But
conversation_loop calls should_compress() TWICE per turn with two different
measures (turn_context.py / conversation_loop.py:1033 and :4789):

  * pre-API : request_pressure_tokens -- a rough estimate that can dip BELOW
              the threshold
  * post-API: real prompt tokens -- which stay above it

So the rough reading reset the strike every turn and the loop never stopped.
Reproduced: 8 compactions in 8 turns under the real two-call pattern, even
with the previous fix applied. (My earlier repro only called should_compress()
once per turn, which is why it looked contained.)

Move the verdict to update_from_response(), the one place that sees the
provider's real prompt_tokens for the just-compacted conversation, guarded by
the existing awaiting_real_usage_after_compression flag so it fires exactly
once per compaction. Real-vs-real: it cannot be fooled by a rough sub-threshold
reading, and (from the previous commit's lesson) never subtracts an estimate
from a real count. should_compress() goes back to a plain threshold test plus
the pre-existing cooldown and anti-thrash guards.

New test test_rough_preflight_reading_does_not_reopen_the_loop drives the real
two-call-per-turn pattern and fails on the prior should_compress()-based commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:59:49 +05:30
Igor Ganapolsky
d172445629 fix(compaction): check the threshold against real tokens, not an estimated floor
Follow-up to the previous commit, whose futility check was unsound:

    incompressible_floor = max(0, display_tokens - pre_estimate)

`display_tokens` is the provider's real prompt count; `pre_estimate` is
`estimate_messages_tokens_rough(messages)`. Subtracting an estimate from a real
count folds the tokenizer skew into "floor" and misreads it as incompressible
overhead. With a 1.6x skew on a 200K window (threshold 150K, true floor 30K):

    rough_msgs=253,804  real_prompt=436,086
    computed floor = 182,282        <-- mostly skew; exceeds the threshold
    after compaction: 401 -> 77 msgs, real prompt = 106,361  (CLEARS 150,000)
    verdict: ineffective_count = 1  <-- false positive

Two such passes would permanently disable compaction on a healthy session --
worse than the loop this PR set out to fix.

Move the check into should_compress(), where both sides of the comparison are
the caller's own token count:

  * prompt under the threshold  -> not thrashing; reset the counter
  * a compaction just ran and we are STILL over -> one strike

Real-vs-real, so tokenizer skew can never be mistaken for a floor, and nothing
subtracts an estimate from a real count. compress() now only ever increments the
counter; the reset lives with the one measure the trigger uses.

Adds `test_no_false_positive_under_tokenizer_skew` (the case above) and
`test_counter_resets_once_the_prompt_fits_again` (one failed pass must not
disable compaction forever). Against upstream, 5 of the 7 cases fail; the 2 that
pass are the regression guards, which is the intended shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:59:49 +05:30
Igor Ganapolsky
32f30d2a4f fix(compaction): anti-thrashing guard never fired; score against the threshold
`should_compress()` documents anti-thrashing protection ("if the last two
compressions each saved less than 10%, skip compression to avoid infinite
loops"). In practice `_ineffective_compression_count` reset on every pass,
so the guard was dead code and a mis-sized context window presented as a
hung CLI instead of a warning.

Two defects:

1. Mixed measurement bases. Effectiveness was
   `(current_tokens - estimate(compressed)) / current_tokens`, where
   `current_tokens` is the provider's FULL prompt (system prompt + tool
   schemas + messages) but `estimate(compressed)` covers messages only.
   Every compaction therefore reported ~96% savings and reset the counter.
   Savings is now scored messages-vs-messages.

2. Message shrinkage is the wrong yardstick. `should_compress()` trips on
   the full prompt, but compaction can only shrink messages -- the system
   prompt and tool schemas are an incompressible floor. When that floor
   alone meets the threshold, each pass shrinks messages by a healthy
   margin, legitimately resets the counter, and still leaves the prompt
   over the line; the next turn compacts again, forever. Observed in the
   wild: 45+ consecutive compactions, one auxiliary-LLM call each, zero
   progress. Effectiveness is now scored against the goal -- did the
   projected prompt get under the threshold? -- and a futile pass warns
   with the numbers that prove it.

Also record an ineffective pass on the "only N messages (need > M)" early
return, which previously returned the transcript unchanged without moving
any anti-thrash state -- the same class of bug the neighbouring
"no compressable window" branch was already fixed for.

Tests: 4 of the 5 new cases fail on main and pass here; the fifth pins
that effective compaction still resets the counter (121 -> 15 messages,
88.9% savings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:59:49 +05:30
teknium1
6142203bd7 fix(gateway): ground readiness in live runtime state 2026-07-11 08:42:21 -07:00
teknium1
f9728af5e2 feat(gateway): add authenticated runtime readiness checks 2026-07-11 08:42:21 -07:00
teknium1
0d63c23f36 fix(insights): harden per-route usage attribution
Preserve deletability, route identity, stored costs, aggregate reconciliation,
and zero-usage Codex route accounting on top of the salvaged per-model usage
work.
2026-07-11 05:59:42 -07:00
Jan Hermes Integration
d14006ead2 fix(telemetry): persist first accounted fallback route 2026-07-11 05:59:42 -07:00
Thomas Connally
cb7f6bbb2e feat(agent): track per-model token usage for mid-session model switches
The `sessions` table records only the initial (model, billing_provider)
for a session, so when a user switches models mid-session (via `/model`
or programmatically) every token — including the switched model's — is
attributed to the first model. Insights/billing reports then hide the
cost of the new model entirely (e.g. a session that started on deepseek
and switched to opus shows $0 for opus).

Add a `session_model_usage` table keyed (session_id, model,
billing_provider) that accumulates each per-API-call delta under the
model active at the time of the call. `update_token_counts()` is the
single chokepoint every per-call delta flows through (CLI, gateway,
cron, delegated, codex), so recording there captures accurate
attribution on every platform. Only the incremental path records — the
gateway's `absolute=True` summary overwrite is skipped to avoid
double-counting cumulative totals that can't be split per model. When a
call omits the model, it falls back to the session's recorded model,
matching the existing COALESCE-from-session summary behaviour.

Insights `_compute_model_breakdown` now aggregates tokens and cost from
`session_model_usage`, so a switched session splits correctly across
models, with a defensive fallback to the per-session aggregate for any
session lacking usage rows. A v17 migration backfills one usage row per
existing token-bearing session from its aggregate totals (idempotent via
INSERT OR IGNORE), validated lossless against a 1.3 GB production DB.

Tests: per-model recording, mid-session split, model fallback, absolute
no-double-count, v17 backfill, and an insights-level switch breakdown.

Fixes #51607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 05:59:42 -07:00
teknium1
31152ae108 fix(providers): align Fireworks integration with project policy 2026-07-11 05:43:35 -07:00
Alex Jestin Taylor
c97d9a4c07 feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.

- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
  so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
  models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
  override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
  valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
  to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
  profile fallback_models, so the default is an agentic chat model instead of
  whatever the live catalog returns first (Fireworks listed an image model,
  flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
  Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
  i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
  doctor incl. the slash-form regression, aux headers, credentials), discovery
  spot-check, and a live smoke test driven through the Hermes runtime.

Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
2026-07-11 05:43:35 -07:00
teknium1
8041be7954 fix(model): keep configured provider authoritative 2026-07-11 05:43:07 -07:00
konsisumer
0ca6a98541 fix(cli): keep current provider visible in model pickers 2026-07-11 05:43:07 -07:00
Teknium
a0a6cd80f5
fix(agent): preserve none vs unknown tool effects (#61783)
* fix(agent): persist truthful tool effect dispositions

* fix(agent): preserve successful siblings during orphan recovery

* fix(agent): narrow effect dispositions to none and unknown
2026-07-11 05:41:58 -07:00
briandevans
5ecc07986f fix(cli): preserve -t/-m/--provider/--tui/--dev before chat subcommand
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.

Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.

Reproduction (origin/main, before fix):

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  None
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

After fix:

  >>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
  'web'
  >>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
  'web'

Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.

Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
  (not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
  `-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
  parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.

Fixes #28780.
2026-07-11 03:51:43 -07:00
Teknium
91d05b982d
fix(xai): recover legacy encrypted replay failures (#62420) 2026-07-11 03:51:38 -07:00
Teknium
c7619773e7
fix(agent): restore primary credential pool after fallback (#62417) 2026-07-11 03:44:02 -07:00
kshitijk4poor
3b2ef789df test(models): patch secured Novita pricing seam 2026-07-11 12:28:55 +05:30
kshitijk4poor
d83cd6f7c3 fix(security): secure Azure catalog probes 2026-07-11 12:28:55 +05:30
kshitijk4poor
1f46145e03 fix(security): order sanitizer after installed hooks 2026-07-11 12:28:55 +05:30
kshitijk4poor
92c2146039 fix(security): sanitize after installed request hooks 2026-07-11 12:28:55 +05:30
kshitijk4poor
4530a4ca4c fix(security): preserve opener-level header policy 2026-07-11 12:28:55 +05:30
kshitijk4poor
cf34a1e8c7 fix(security): cover remaining catalog credential paths 2026-07-11 12:28:55 +05:30
kshitijk4poor
27a1042b13 fix(security): preserve installed urllib policies 2026-07-11 12:28:55 +05:30
kshitijk4poor
6fe2c8b78c test(models): patch the secured request seam 2026-07-11 12:28:55 +05:30
kshitijk4poor
6e75ba7fa0 fix(security): enforce one redirect credential policy 2026-07-11 12:28:55 +05:30
dsad
5415b77658 fix(models): strip credentials on catalog redirects 2026-07-11 12:28:55 +05:30
solyanviktor-star
61e3bd67d6 compare full origin (scheme, host, port), not hostname, before keeping credentials
Review feedback: a same-host redirect to a different port can land on a
different service, which must not inherit the provider API key. Compare
(scheme, hostname, effective port) — with 80/443 defaults — instead of
hostname alone, and add a two-server regression test for the
same-host/different-port case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:28:55 +05:30
solyanviktor-star
a061788d43 security(providers): strip credential headers on cross-host redirects in fetch_models
fetch_models() sends Authorization: Bearer <api_key> plus any
default_headers (x-api-key etc.) via urllib.request.urlopen, and
urllib's redirect handler forwards every header when following a
3xx — including to a different host. A catalog endpoint (or a
compromised/misconfigured proxy in front of it) answering with a
redirect to another origin therefore received the provider API key.

Install an HTTPRedirectHandler that drops authorization, x-api-key,
api-key, x-goog-api-key and cookie when the redirect target hostname
differs from the original request, mirroring the pattern already used
in skills/creative/comfyui/scripts/_common.py. Same-host redirects
keep credentials so legitimate path-level redirects still work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:28:55 +05:30
kshitijk4poor
9eaf3abf6e test(codex): pin final replay preflight boundary
Exercise request and execution middleware replacements through the real
conversation loop and assert the provider payload is sanitized.
2026-07-11 12:09:27 +05:30
kshitijk4poor
bce17bf6a2 fix(codex): enforce Copilot replay policy at dispatch
Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
2026-07-11 12:09:27 +05:30
kshitijk4poor
22d5a35c16 fix(codex): harden Copilot replay classification
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
2026-07-11 12:09:27 +05:30
joaomarcos
83b2a685cd fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.

Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.

Still #32716.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 12:09:27 +05:30
joaomarcos
b9146a47bc fix(codex): never replay message-item id on Copilot Responses connections
Copilot (api.githubcopilot.com/responses) binds replayed assistant
codex_message_items ids to a specific backend "connection". Credential-
pool rotation, a gateway restart, or routine load-balancer churn between
turns all invalidate that binding, and Copilot rejects the stale id with
HTTP 401 "input item ID does not belong to this connection" — even for
short ids well under the #27038 64-char length cap, since this is a
connection-scope problem, not a length problem. Once a session captures
one of these ids it is persisted and replayed forever, permanently
bricking the session.

Thread an is_github_responses flag from build_kwargs/convert_messages
into _chat_messages_to_responses_input and drop the id unconditionally
on that path, mirroring how reasoning items already strip id on replay.
phase/status/content are still replayed so cache-relevant signal isn't
lost — only the connection-scoped id is unsafe to reuse.

Written to apply independently of the #27038 length-cap fix so the two
PRs don't block each other; they touch adjacent conditions in the same
block and merge cleanly in either order.

Fixes #32716

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 12:09:27 +05:30
kshitijk4poor
4aa499ff9f fix(telegram): harden flood fallback recovery
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
2026-07-11 11:13:50 +05:30
Gille
04898631cb fix(telegram): recover final delivery after stream flood 2026-07-11 11:13:50 +05:30
kshitijk4poor
02063ece11 fix(cron): scope inline calls to reported transport 2026-07-11 11:07:34 +05:30
kshitijk4poor
47c91e4c34 fix(cron): abort inline requests on timeout 2026-07-11 11:07:34 +05:30
PRATHAMESH75
5c5dd6b7ec fix(agent): run cron LLM calls inline to avoid gateway deadlock (#62151) 2026-07-11 11:07:34 +05:30
HexLab98
1234f39e31 test(cron): guard direct API path for gateway deadlock (#62151) 2026-07-11 11:07:34 +05:30
joaomarcos
0b2907f586 fix(codex): drop oversized message ids on Responses input replay
Codex assigns assistant message items server-side ids that can run
400+ chars (base64 encrypted blobs), but the Responses API caps
input[].id at 64 chars and rejects the whole request with a
non-retryable HTTP 400. Once a session captures one of these long
ids, every subsequent turn replays it and 400s forever, since the
history persists it in codex_message_items.

Add a 64-char length guard at both replay sites — the history-to-
input converter and the final preflight gate — so oversized ids are
dropped while short ids (msg_...) are kept for prefix-cache hits.
Mirrors the existing pattern for reasoning items, which already
strip their id before replay because store=False means the API
can't resolve ids server-side anyway.

Fixes #27038

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 19:26:08 -07:00
Teknium
dabae386ef
fix(cron): bind claim heartbeats to dispatch owner (#62155) 2026-07-10 19:12:35 -07:00
Teknium
5ba2d167ba
Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)
This reverts commit cd7a8dfde0.
2026-07-10 19:12:32 -07:00
brooklyn!
291eae63b7
Merge pull request #62016 from NousResearch/bb/desktop-vibe-hearts
feat: vibe reactions — floating hearts on affection, across CLI/TUI/desktop
2026-07-10 16:14:27 -05:00
Teknium
b9b463f3bd
feat(security): expose deterministic tool output risk (#61793)
* feat(security): expose deterministic tool output risk

* fix(security): emit output-risk events only for findings
2026-07-10 07:58:12 -07:00
Wilson Kinyua
1a2f3aea9a fix tui finalize persist drop conversation_history so disconnect saves chat
finalize passed conversation_history=history aliasing the snapshot so flush
skipped every message and wrote nothing. now flush _session_messages via
marker dedup like gateway shutdown. add real db e2e tests.
2026-07-10 07:47:10 -07:00
giggling-ginger
cd7a8dfde0 fix(agent): release pool FDs on owning-thread client close (#61979)
force_close_tcp_sockets stayed shutdown-only after #29507 to avoid
cross-thread FD recycle. That left CLOSED sockets unreclaimed when
httpx.close() skipped already-shutdown sockets under long-lived
gateways (~1 CLOSED fd / 6 min via proxy).

Add release_fds= for the owning-thread dispose path only; abort still
defaults to shutdown-only.
2026-07-10 07:28:39 -07:00
PRATHAMESH75
9b72995a1d fix(cron): never stale-remove a one-shot whose run is still alive
get_due_jobs()'s one-shot stale-entry recovery (#38758) treated an
expired run_claim (#59229) as proof the claiming tick died, but a run
stalled on network I/O — or a laptop asleep mid-run — legitimately
outlives the TTL while very much alive. The recovery then deleted the
job record mid-flight: list showed the job gone, and when the run
finished mark_job_run() found nothing to update, so last_run_at /
last_status / last_delivery_error were never recorded.

Two guards, per the liveness signals available:

- Same process (the common single-gateway case): before removing a
  dispatch-limit-reached one-shot, consult the scheduler's running set
  via a lazy import; if the job is still running here it is slow, not
  stale — keep the entry.
- Cross process: run_job's monitor loop now refreshes run_claim.at
  every 60s while the run is alive (including under
  HERMES_CRON_TIMEOUT=0, which previously blocked without polling), so
  an expired claim really does mean the owner died and the TTL stays a
  dead-owner detector.

Fixes #62002
2026-07-10 07:28:33 -07:00
kshitijk4poor
90bd5b0f9b test(telegram): mirror PTB errors in heartbeat recovery 2026-07-10 19:28:03 +05:30
kshitijk4poor
54e1864577 fix(acp): unwrap web extract object titles 2026-07-10 19:14:06 +05:30