Commit Graph

4529 Commits

Author SHA1 Message Date
Teknium fc70d05bb1 fix: arm primary cooldown once while walking fallback candidates 2026-09-07 07:08:25 -07:00
Teknium 68b16aa7e3 fix: retain exhausted-chain cooldown classification after extraction 2026-09-07 07:08:25 -07:00
Teknium fb2f66f586 fix: describe remaining retry eligibility without promising recovery 2026-09-07 07:08:25 -07:00
Halldrix ae4f777c69 fix(agent): surface armed rate-limit cooldown in fallback notice (#104120)
_arm_rate_limit_cooldown now returns the armed backoff seconds so
try_activate_fallback can append them to the user-facing notice
(Primary retried in ~N min/h) instead of discarding the one number
that decides whether the user waits or re-plans. Duration, never
wall-clock: the stored deadline is monotonic-based. Non-rate-limit
reasons and chain-switches from an active fallback arm nothing, so
no suffix is printed there.
2026-09-07 07:08:25 -07:00
Teknium cb1a42d33b refactor: isolate custom health identity and trim redundant alias tests 2026-09-07 07:06:58 -07:00
fangliquanflq 84bbf176a3 fix(agent): preserve route URL path identity 2026-09-07 07:06:58 -07:00
fangliquanflq e3ff3e78d1 fix(agent): quarantine failed fallback destination 2026-09-07 07:06:58 -07:00
fangliquanflq e033bbaa3c fix(agent): quarantine bare custom aliases by endpoint 2026-09-07 07:06:58 -07:00
fangliquanflq 3fcbf13ddb fix(agent): scope named custom health by endpoint 2026-09-07 07:06:58 -07:00
fangliquanflq b40998bc3c fix(agent): isolate custom endpoint billing health 2026-09-07 07:06:58 -07:00
Teknium 27f32bd50b test: exercise output-cap removal across native and child surfaces 2026-09-07 06:15:43 -07:00
Teknium fd3565deec fix: remove dedicated user-facing output cap controls 2026-09-07 06:15:43 -07:00
Teknium 4270e9ad10 fix: distinguish callable Anthropic bearer sources from OAuth text 2026-09-07 06:07:16 -07:00
Teknium 8c2f3082d8 fix: read only installed SDK credential providers at handoff
Independent review found auto-created mock attributes could replace static credentials. Read the SDK instance's stored provider without triggering attribute synthesis.

Co-authored-by: CloudWishOS <99405975+snipecoder@users.noreply.github.com>

Co-authored-by: BGwill-OUTLOOK <bgwillwork@outlook.com>
2026-09-07 06:07:16 -07:00
Teknium b4464f6fac fix: retain rotating credentials during fallback handoff
Adapt the callable-source diagnosis from snipecoder (#102244) and fallback slice from BGwill-OUTLOOK (#102721), without unrelated reasoning or override changes.

Co-authored-by: CloudWishOS <99405975+snipecoder@users.noreply.github.com>\nCo-authored-by: BGwill-OUTLOOK <bgwillwork@outlook.com>
2026-09-07 06:07:16 -07:00
Teknium d0081d9597 refactor: colocate fallback handoff with client lifecycle 2026-09-07 06:07:16 -07:00
liuhao1024 342967058c fix(agent): interpolate the live backups dir into corruption recovery guidance
The corrupt-cause recovery guidance hardcoded `~/.hermes/backups/` while
every other path in the same message follows the active HERMES_HOME
(`{db_path}` is already interpolated). A custom-home or named-profile
deployment was told to restore from a directory that may not exist at all,
mid data-loss incident. Both sites (turn-completion explainer and gateway
startup broadcast) now interpolate `<hermes_root>/backups` via
get_default_hermes_root(), matching hermes_cli/backup.py's real backup
location.

Fixes #104250
2026-09-07 06:06:20 -07:00
Teknium 4fbb253904 fix: mirror successful memory alias writes to providers 2026-09-07 06:05:13 -07:00
liuhao1024 fb0fc7e140 fix(agent): forward the new_text alias in the memory inline executor
The memory_tool schema advertises new_text as an alias for content, and
memory_tool resolves it when content is None. But the table-driven inline
executor's arg_specs (agent/inline_tool_executors.py) did not list new_text,
so _call_tool's allowlist silently dropped it: a replace call using the
documented alias reached memory_tool with both fields None and failed with
"content is required for 'replace' action." — even though the caller
supplied the value. Forward new_text alongside content/old_text so the
documented alias fires and content still wins when both are set, matching
what the batch path (op.get("content") or op.get("new_text")) already
accepts.
2026-09-07 06:05:13 -07:00
fangliquanflq 2b7b940046 fix(agent): reject unsupported Codex video input 2026-09-07 06:04:44 -07:00
Teknium ebe4e7bb44 fix: sanitize resolved auxiliary chat requests before dispatch
Extend the destination-boundary approach from PR #87840 to synchronous, asynchronous and streaming auxiliary dispatch, including prepared MoA requests. Keep native adapter replay intact.

Co-authored-by: siyoon <siyoon@friendli.ai>
2026-09-07 06:02:22 -07:00
Teknium 95ce1af772 style: trim summary dispatch trailing whitespace 2026-09-07 05:59:08 -07:00
Teknium bf169bef77 fix: preserve false bypass values and single hook invocation 2026-09-07 05:59:08 -07:00
fangliquanflq 4d3409f7ff fix(agent): preserve legacy summary hook overrides 2026-09-07 05:59:08 -07:00
Teknium 77086b1439 refactor: isolate compression summary dispatch 2026-09-07 05:59:08 -07:00
Teknium 8d24bc24e1 fix: wait sixty seconds before provider silence notices 2026-09-07 05:24:15 -07:00
Teknium 9da8df8d26 fix(prompt): preserve shared project prefixes across worktrees
Slim salvage of #104688: place project context before workspace state and
keep cwd outside the stable prefix. Put runtime hints behind a final
renderer-owned boundary so quoted operator, memory, plugin and embedder
examples cannot override the persisted runtime cwd or identity fields.

Retain legacy unmarked prompt validation, add two invariant tests and a
credential-free real-AIAgent/git-worktree replay harness. No provider
cache-hit or billing measurements are claimed.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Co-authored-by: HexLab98 <liruixinch@outlook.com>
2026-09-07 04:45:48 -07:00
Teknium dca7a90cf8 fix(agent): reclaim background processes by execution owner
Track raw task identities across an agent's turns and match them against
process owner_task_id during close. Session IDs and shared terminal keys
are not process ownership, so the old bulk cleanup missed delegated work.
Preserve parent/sibling processes and consume teardown notifications.

Move task-resource cleanup into the lifecycle mixin, add real-process
isolation regressions, and document background process lifetime.
2026-09-07 04:38:59 -07:00
Teknium 22c5684b98 fix(agent): clear resumed stream wait status without synthetic reasoning 2026-09-07 02:37:13 -07:00
Teknium ba395af50f refactor(agent): isolate streaming wait monitor phase 2026-09-07 02:37:13 -07:00
Teknium 77f79ae831 fix(agent): keep active Codex reasoning out of wait warnings
Use request-local stream silence for the waiting notice, preserving quiet
activity heartbeats and all existing watchdog policies. Distinguish a stream
that stopped from a request with no response, and clear this request's notice
on the next poll when events resume. Existing fresh first-event retry phases
also reset the display; recovery deadlines explicitly use total call elapsed.

Add two invariant tests (eight cases), proven red on main, plus EN/ZH docs.
Local SDK SSE through classic CLI callbacks in a PTY verifies active reasoning,
true silence, and an already-visible warning clearing on resumed reasoning.

Related: #92657 addresses repeated waiting notices; its phase deduplication
still labels active streams as no response and is not incorporated here.
2026-09-07 02:37:13 -07:00
Teknium 47d73afeb1 refactor(agent): isolate non-stream request polling 2026-09-07 02:37:13 -07:00
liuhao1024 3c9c225425 fix(agent): tolerate schema-valued "type" keys in the watchdog payload estimator (#104793)
The image-part membership test in _payload_chars() ran on every dict in
the request, so a tool JSON Schema with a parameter named "type" (its
value is the sub-schema dict) or a multi-type value like
["string", "null"] raised TypeError: unhashable type before the
request reached the provider. Guard the membership test with
isinstance(str): structured "type" values are payload data, not
content parts, and keep the legacy walk for them.
2026-09-07 00:49:51 -07:00
Teknium 08b140d14e fix: gpt-6 Astra on Codex OAuth gets the 85% compaction autoraise
Codex OAuth caps gpt-6-astra at the same 272K window as gpt-5.4/5.5/5.6,
so the global 50% trigger compacted at ~136K. Extend the existing
codex_gpt55_autoraise gate to any slug containing "astra" (minus the
opt-in -900k picker variants, which already unlock the wider window).
Other routes (OpenAI direct, OpenRouter) keep the user threshold.
2026-09-06 23:08:45 -07:00
crdesign8 922c0d670c fix(agent): stale-call watchdog prices images at the learned per-image cost, not base64 length (salvage #76471)
estimate_request_context_tokens (the stale-stream / non-stream watchdog and Codex TTFB floor
estimator) did len(str(payload)) // 4 over the wire payload, so one native screenshot read as
~100K tokens and selected the 1200s giant-conversation floor while the provider's real prompt
was ~2K (#63871, #76411). Image content parts on both wire shapes (Chat image_url / Responses
input_image) now cost the per-image price learned from provider usage (agent/image_token_cost,
bound per turn; worker threads inherit it via copy_context); base64-looking STRINGS stay text
and tool schemas mentioning image are not images.

A/B, one 300KB screenshot: main estimate 100,545 -> codex floor 1200s / stale 300s; now 2,025
-> floor 0s / stale 180s.

Design and boundary (structured parts only, tools/instructions opaque, plain data-URL strings
stay text) from #76471 by @crdesign8; re-authored to reuse the learned cost instead of a second
fixed constant, and trimmed to 2 invariant tests.
2026-09-06 22:34:20 -07:00
sylvainCDA 693641aa8b fix(chat-completions): strip name from tool-result messages for strict providers
The Chat Completions schema has no `name` field on `role: tool` (only on
the long-removed `role: function`), but Hermes carries the tool name over
onto the result message. Permissive providers ignore it; strict ones
(aki.io) reject the whole payload with `contains item with unknown key
name`, which breaks every tool call in the session.

Follow-up to the review on #51365:

- The strip now goes through the copy-on-write `mutable_msg()` path in
  `convert_messages()`, preserving the identity/copy-on-write contract
  instead of mutating `msg` in place.
- `handle_max_iterations()` hand-builds its summary payload and calls
  `chat.completions.create()` directly, bypassing the transport, so it
  leaked `name` even with the transport fixed. It now mirrors the same
  role-qualified removal, next to the existing tool_name/codex_*/timestamp
  strips.

The removal is role-qualified: `name` stays on user/assistant messages,
where it is schema-valid.

Regression coverage on both paths; both tests fail without the fix.
2026-09-07 02:50:45 +05:30
Teknium be58c276ee feat(compression): per-image token cost learned from the provider's own usage (#70328, supersedes #70463)
A flat per-image constant (1500 in the trigger estimator, 1600 in the tail-budget walk) is wrong in
both directions: a screenshot costs ~1,100 tokens on one provider and 4,000+ on a local mmproj
model. In a GUI loop on a 64K window the estimate sat at ~20K while the real prompt passed 80K,
so compaction never fired and the provider rejected every request (#70328).

The provider prices every image exactly on the request that carries it, so the cost is
observable from usage alone, with no vendor formula: with a fresh usage anchor, the residual
between the next real prompt_tokens and anchor + text-only delta is the price of the N images
that delta introduced.

- agent/image_token_cost.py: calibrate_from_usage() runs in record_response_usage before the new
  anchor is captured; the learned value (EMA, plausibility-banded) is kept per model@host in
  ~/.hermes/cache/image_token_costs.json and bound per turn through a ContextVar.
- estimate_messages_tokens_rough, _content_length_for_budget (tail walk) and gateway hygiene all
  read the same bound value, so trigger and walk agree; the per-message memo now caches text
  tokens and image COUNT so a recalibration re-prices cached rows.
- One flat default (1500) remains only until the first vision turn; the duplicate 1600 is gone.

evals/token_accounting/ab_image_cost_calibration.py (real AIAgent, fake provider pricing images
at 4,000, one screenshot per turn, 64K window): main learns nothing (1500) and the tail walk
under-prices its own protected tail by 56.5%; this branch learns 4,374 after one vision turn
and the walk's error is +8.5%.

Reporter and first-fix credit: @JonthanaHanh (#70328, #70463).
2026-09-06 14:19:42 -07:00
Joey 820106d4a5 fix(compaction): count api_content in tail budget 2026-09-06 14:04:05 -07:00
Teknium 6d2645e64e feat(logging): the API call line carries cache write count, the provider's response id, and the serving upstream
Diagnosing the 1,393-agent run's cache misses took a state.db join and a live
probe because none of the three were on the one line we log per call:

  write=<n>    cache_creation tokens; a write costs 50x a read, so this is the
               money, and "read stuck, write large" on consecutive lines is a
               routing miss visible without a probe
  id=<id>      the provider's response id (Anthropic msg_..., OpenRouter/Nous
               gen-...): what a provider needs to look a request up
  upstream=<n> who actually served, when the route reports it (OpenRouter's
               `provider`); how we learned GMI was not serving

Fields are appended to the existing line and omitted when absent, so every
existing parser (evals/postmortem, the two cache_prefix probes) keeps
matching; the forensics parser reads them when present.

The streamed chat path fabricated `id="stream-<uuid>"` and dropped the
chunks' id/provider on the floor; it now keeps the first chunk's id and
provider, falling back to the fabricated id only when the stream never sent
one. Nothing depended on the prefix.

Live (Nous, Fable 5.1, both wires): chat -> `write=3759
id=gen-1788727882-... upstream=Anthropic`; native -> `id=gen-1788727891-...`
(the Anthropic Message object's id was already real). Tests: fields present
and omitted, prefix unchanged; forensics parser reads new and old lines.
2026-09-06 13:52:15 -07:00
Teknium 2ed33fb38e refactor(memory): keep the spill, drop registration-trust rework
The prefetch spill is the fix; the is_builtin registration flag and duplicate-
instance rejection defended against a provider naming itself "builtin", which no
live path can do (only a test fixture does). Restore the name-based check and the
four test files that only changed for the new kwarg; keep two spill invariants.
2026-09-06 13:25:48 -07:00
Joey d932fa5929 fix(memory): spill oversized external prefetch 2026-09-06 13:25:48 -07:00
Teknium 562e6e4824 fix(compression): opaque encrypted_content costs 0 in every local token estimate
Codex Responses reasoning and compaction items carry ciphertext the provider prices by its own
token count, never by bytes; a single native compaction checkpoint is ~5M chars, which the
bytes/4 estimator turned into ~1.29M "tokens" against a 204K threshold (#100611). #104192 deferred
that decision for one request; this removes the mis-pricing at the source so the preflight
estimator and the tail-budget walk agree (a mismatched size class protects blob-heavy rows as
"small" and compaction re-fires). Only real usage prices these items, and the usage anchor carries
that price forward.

evals/native_compaction/ab_checkpoint_preflight.py: preflight estimate after checkpoint
1,292,413 -> 58 rough tokens; the over-threshold negative arm still compresses.
2026-09-06 13:21:17 -07:00
Teknium 0f4587e336 refactor(compression): every compaction gate asks real usage first; rough estimates only decide whether to wait
Two parallel "real usage" mechanisms fought each other: the usage anchor (real + delta) and the
compressor's rough/real projection (should_defer_preflight_to_real_usage with
last_rough_tokens_when_real_prompt_fit / _pending_request_rough_tokens / note_request_rough_estimate
baselines). The projection stored an anchored, real-scale figure as its "rough" baseline, so a
rewind that invalidated the anchor produced phantom growth and a spurious compaction (#103391).

Now there is one authority:

- Post-tool gate (turn_preflight.compress_after_tool_results): anchored figure first (the raw
  last_prompt_tokens ignored the tool results just appended), then real, then rough.
- Gateway hygiene (run_turn._hmwa_hygiene_plan): real session count, else the anchor persisted on
  the session row, else rough.
- Preflight / pre-API gates: an anchored figure is never deferred. A whole-context rough estimate
  over threshold waits ONE request for the provider's real count instead of compressing on a guess
  (first request, rewind/edit-resend, reloaded history without a persisted anchor).
- The wait is one request, never a disable: a provider that omits usage
  (note_usage_less_response, #2153 class), a real reading already over threshold, a rough figure
  past the whole window, and provider-proven overflow all compress immediately; the post-compaction
  latch (#36718 / #104192) is unchanged.
- Projection baselines and their bookkeeping deleted (-101 LOC in context_compressor); the fixtures
  that scripted whole-history estimates now state the fact they relied on (provider omits usage).

Fixes #103391 (closes #103397 by construction — the baseline it repaired no longer exists).
2026-09-06 13:21:17 -07:00
686f6c61 c0aaa238f6 feat(compression): usage anchor survives DB reloads and process restarts (salvage #99585)
The usage anchor (real usage.prompt_tokens + delta estimate of what was appended since)
identified the priced transcript by id() of the last message, so it was None on EVERY
gateway turn (history is re-read from the DB each turn) and in every fresh process
(--resume, desktop per-turn serve). Those are exactly the surfaces where the bytes/4
estimate then fired local compression against payloads the provider priced far under
threshold (#99421, #104462).

- agent/usage_anchor.py owns the anchor: content fingerprint instead of id(), persisted on
  the session row (model_config._usage_anchor) via set_usage_anchor(), restored on the first
  resumed turn while the durable transcript still matches, cleared with the row on
  compaction / codex-native rewrite / session reset.
- Callers repointed from model_metadata (the compat table follows).

Design and persistence slot from #99585 by @686f6c61; re-authored against the Sep 2026
layout (the branch predates the model_metadata / agent_init split).
2026-09-06 13:21:17 -07:00
liuhao1024 a9e56c048a fix(compression): anchor codex app-server preflight on reported usage
The codex_app_server runtime bypasses the conversation loop, so the
usage-anchored context accounting captured there never ran: agent._usage_anchor
stayed None forever. Every preflight estimate therefore fell back to the rough
mirror-transcript heuristic, which is deliberately never compacted on this
runtime (_record_codex_app_server_compaction preserves the mirror), so it grows
monotonically while the real thread may be tiny or freshly compacted. With
compression.codex_app_server_auto=hermes that estimate alone tripped the
threshold and fired thread/compact/start on nearly every turn of a long-lived
session (#100381).

Mirror the main loop's post-response capture: _record_codex_app_server_usage
now snapshots the reported thread usage into agent._usage_anchor (base prompt
+ completion exactly as the provider counted them, with estimation confined to
messages appended since). A usage-less turn keeps the previous anchor, and the
post-compaction invalidation site is unchanged.
2026-09-06 13:21:17 -07:00
Teknium b167e81750 fix: legacy Bot Mode section in SOUL.md no longer taxes every session or shadows the live roster
Older desktop builds appended a frozen "## Messaging other agents" section (roster
included) to SOUL.md. Since the server started injecting the live section into Bot Chat
sessions, that copy did two wrong things: every CLI/TUI/messenger session paid ~600 tok
for a bot-only protocol, and in Bot Chat itself the probe went silent when SOUL carried
the heading, so bots saw the stale roster instead of the live one.

- load_soul_md strips the legacy section at read time (covers un-migrated profiles and
  the ambient-home edge cases the same way the SOUL isolation fix does)
- bot_mode_probe drops the SOUL-carries-heading suppression; a SOUL-era stored Bot Chat
  prompt now counts as legacy and is upgraded once (stamped, so it cannot loop)
- config migration v41 rewrites SOUL.md across the default + every profile once
2026-09-06 13:08:31 -07:00
kshitijk4poor 3ac671dbba refactor(agent): drop the write-only agent-level codex watchdog timestamps
With a request-local watchdog state on every codex request, agent._codex_stream_last_event_ts / _last_progress_ts had no remaining reader; the state-None snapshot only runs for non-codex requests where no watchdog consults it.
2026-09-07 00:41:09 +05:30
kshitijk4poor 1ad8b5dd4a refactor(agent): trim the watchdog phase split to what the tests pin
- Explicit-vs-implicit idle timeout detection uses env_float with a sentinel
  instead of a hand-rolled parse (unset and unparseable both mean implicit).
- _on_event writes the agent-level timestamps only when there is no
  request-local watchdog state; with state present they were dual bookkeeping
  nobody read (the snapshot prefers the state).
- _abort_request keeps main's close-then-retire order: the reorder had no
  test teeth and is a separate concern from #90449.
2026-09-07 00:41:09 +05:30
PeaceMaker-best 463d4bcd15 fix(agent): defer codex idle watchdog until progress
Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-09-07 00:41:09 +05:30
Teknium d97f318717 Merge pull request #103526 from NousResearch/fix/nous-auth-stampede
fix(nous): adopt a same-account fresh key before expiry and start the keepalive in every process (620 hourly 401s → 0; review-found account takeover closed)
2026-09-06 12:02:58 -07:00