Commit graph

17451 commits

Author SHA1 Message Date
Gabriel Steenhoek
30bb55588f fix(gateway): retry detached restart watcher without breakaway
The Windows /restart watcher's outer Popen spawns the watcher with
windows_detach_popen_kwargs() (which carries CREATE_BREAKAWAY_FROM_JOB),
but a restrictive parent job object can reject that bit with OSError and
the current call has no retry. Preserve the current watcher
implementation and add a focused breakaway-denied fallback.

Preserved from current main: watcher_python / pythonw.exe selection, the
str(restart_after_s) deadline, the scrubbed watcher_env, the intentional
no-breakaway inline respawn, and the entire POSIX setsid/bash path.

- primary keeps **windows_detach_popen_kwargs()
- on OSError, retry the same argv/env with
  creationflags=windows_detach_flags_without_breakaway()
- on dual failure, log a definitive, path-safe warning (interpreter
  basename + numeric winerror/errno only) and return without crashing

Replace the superseded breakaway-first inline design and its AST tests
with focused behavioral coverage that drives the real coroutine with a
mocked subprocess.Popen (retry, argv/env/DEVNULL preservation, POSIX
single-session kwarg, no-breakaway inline respawn, secret-safe logging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:59:47 -07:00
Teknium
f7b90e6f80 feat(moa): add privacy redaction filter with display/full modes
Adds moa.privacy_filter ('' | display | full, default off — issue #59959):

- display: redact user-visible surfaces only (reference blocks emitted to
  the UI + saved MoA trace records, including per-advisor full input/output
  and the aggregator-input copy); the aggregator sees raw advisor text so
  synthesis quality is unaffected.
- full: additionally redact the advisor text injected into the aggregator
  prompt, on both the persistent facade path and the one-shot /moa
  synthesis path (the issue's literal ask). Legacy boolean true maps here.

Secret/credential shapes (API-key prefixes, JWTs, private keys, DB
connection strings) are delegated to the central redactor
(agent.redact.redact_sensitive_text, force=True + code_file=True); the MoA
filter adds only email and clearly delimited phone-number patterns. No
bare 10-digit matching: line numbers, timestamps, epoch values, git SHAs,
IPs, versions, and source-code assignments in code-review-shaped advisory
text pass through byte-identical. The reference cache always holds raw
text — redaction happens at each consuming surface, so a mid-session mode
change never leaks or double-redacts.

Reworked from PR #60463: replaced its hand-rolled pattern list (which
matched bare digit runs and re-implemented key shapes) with central-
redactor reuse + safe patterns, and split the single boolean into
display/full modes. Credited for the feature framing.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 17:50:40 -07:00
Teknium
850f576f3d feat(moa): add every_n fanout cadence with cached-guidance reuse
Extends the fanout enum with 'every_n:<N>' (N >= 2): advisors run on the
first iteration of each user turn and every Nth tool iteration after it;
off-cadence iterations REUSE the cached guidance from the last on-cadence
run via the same cache mechanism the user_turn fanout uses, so the
aggregator still gets advice on every step. The cadence counter is scoped
per user turn (resets on a new user message) and only advances when the
advisory state actually changes, so streaming retries never consume a
cadence slot. Mapping form {mode: every_n, n: N} normalizes to the
canonical string. Unknown/degenerate values fall back to per_iteration.

Addresses issue #63393 (advisor fan-out multiplies turn latency/cost by
the tool-iteration count). Redesigned from PR #63448: the submitted shape
skipped references entirely on off-cadence iterations (aggregator ran
advice-less); this version keeps the last advice in play, credited for
the idea and cadence framing.

Config-gated, default-off (default fanout remains per_iteration).

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 17:50:40 -07:00
Teknium
74a56b76b0 test(moa): regression for aggregator-model thought_signature resolution (#66212)
Adds test_moa_gemini_aggregator_sanitize_uses_real_model: drives a full MoA
tool-call turn (virtual-provider mode) with a Gemini aggregator and asserts
the strict-API sanitize pass is invoked with the resolved aggregator model
(gemini-3-pro-preview), never the virtual preset name once a slot is
resolved — the exact path that stripped extra_content/thought_signature and
made Gemini aggregators 400 (#65092).

Writing the test surfaced a gap in the salvaged #66212 fix: in virtual-
provider MoA mode (provider=moa, no moa_config threaded through
run_conversation) the conversation-loop branch never fired because it only
consulted moa_config. Extend it to fall back to the facade's
last_aggregator_slot — the same source the handle_max_iterations fix uses —
so both MoA entry modes resolve the real aggregator model.

Also adds the contributors/emails mapping for the #15676 credit base.
2026-07-23 17:26:24 -07:00
Teknium
16950a4568 fix(moa): normalize Copilot aliases + carry x-initiator through retry rebuilds (#60293)
Follow-ups to the salvaged core of #60293:

- Gate the x-initiator header on _normalize_aux_provider() instead of a
  literal 'copilot' string compare, so slot configs spelled github /
  github-copilot / github-models / copilot-acp / mixed case all get the
  user-turn attribution.
- Thread extra_headers through _retry_same_provider_sync/_async so the
  credential-refresh and pool-rotation retry rebuilds don't silently drop
  the header (the rebuilt kwargs previously started from scratch).
- Add a transport-boundary test asserting the header reaches the SDK
  client's create() kwargs (no call_llm mocking), an alias-spelling
  matrix test, and a retry-rebuild preservation test.
2026-07-23 17:26:24 -07:00
dyreckt
4c66307c36 fix(moa): pass Copilot initiator header to advisors 2026-07-23 17:26:24 -07:00
Teknium
0749cac7a1 fix(moa): share facade factory so restore/recover keep reference relay (#53802)
Follow-up to the salvaged core of #53802: a naive MoAClient(preset) rebuild
restores a working facade but silently drops the reference_callback relay
wired in agent_init, so moa.reference / moa.aggregating display events stop
reaching every frontend for the rest of the session.

Introduce agent.moa_loop.build_moa_facade(agent, preset) as the single
construction point for the MoA facade and use it at:
- initial client construction (agent_init.py)
- turn-start fallback restore (restore_primary_runtime)
- transient transport recovery (try_recover_primary_transport — previously
  fell through to _create_openai_client with MoA's empty client_kwargs and
  died with 'api_key client option must be set')
- mid-session model switches (switch_model)

The relay reads agent.tool_progress_callback at emit time, so callbacks
attached after construction are picked up automatically.

Adds test_moa_restored_facade_still_emits_reference_events covering event
delivery through a restored facade.
2026-07-23 17:26:24 -07:00
kosta
5501187847 fix(moa): restore virtual runtime after fallback 2026-07-23 17:26:24 -07:00
Dineth Hettiarachchi
8d14e19f9a fix(agent): close MoA stream on interrupt 2026-07-23 17:26:24 -07:00
Idris Almalki
8d119832b4 fix(gemini): emit thoughtSignature sentinel for cross-provider tool_calls in native adapter
When Hermes fails over from a non-Gemini provider (xAI, Anthropic, etc.) to
Gemini mid-conversation, the existing assistant tool_calls in history carry
no Gemini ``extra_content.google.thought_signature`` (the originating provider
never emits one).  The native adapter's ``_translate_tool_call_to_gemini``
omitted ``thoughtSignature`` entirely in that case, so Gemini 3 thinking
models rejected every replayed turn with::

    HTTP 400 INVALID_ARGUMENT
    Function call is missing a thought_signature in functionCall parts.
    Additional data, function call default_api:<tool_name>, position N.

The Cloud Code Assist sibling adapter already handles this exact case by
emitting a sentinel ``"skip_thought_signature_validator"`` (see
``agent/gemini_cloudcode_adapter.py:106``, originally added in #11270 and
documented as matching ``opencode-gemini-auth``'s approach).  This change
mirrors that fallback in the native adapter so the two paths behave
identically when replaying cross-provider history.

Verified live against ``generativelanguage.googleapis.com/v1beta`` with
``gemini-3-pro-preview``: synthetic 2-turn conversation with no real
``thoughtSignature`` returns 400 without the sentinel and 200 with it.

Test added: ``test_build_native_request_emits_sentinel_for_cross_provider_tool_call``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-23 17:26:24 -07:00
AlexFucuson9
f65d105cbb fix(agent): preserve Gemini thought_signature in MoA aggregator mode
When MoA mode is active with a Gemini model as the aggregator,
agent.model holds the virtual preset name (e.g. "closed"), not the
actual aggregator model name. The _sanitize_tool_calls_for_strict_api
call uses agent.model to decide whether to keep extra_content
(thought_signature) on tool_calls — since "closed" doesn't contain
"gemini", the thought_signature is stripped and the Gemini aggregator
rejects the next request with HTTP 400 (INVALID_ARGUMENT):
"Function call is missing a thought_signature in functionCall parts."

Fix: resolve the actual aggregator model name from moa_config
(conversation_loop) or last_aggregator_slot (chat_completion_helpers)
and pass it to _sanitize_tool_calls_for_strict_api so the
_model_consumes_thought_signature check sees the real Gemini model.

Closes #65092
2026-07-23 17:26:24 -07:00
Teknium
d0d116be2e fix: getattr-guard min_tail_user_messages for __new__ test doubles
Bare ContextCompressor.__new__ doubles (test_compress_focus,
cross_session_guard, image_tokens, pre_compress_memory_context) skip
__init__ and lack the attribute — the documented compression-path
test-double pitfall. Guard with getattr default 1 + int type pin
(bool excluded).
2026-07-23 17:03:49 -07:00
Teknium
d43cc2ca80 fix(compress): gate N-user tail guarantee to actionable turns, behavior-preserving default
Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
2026-07-23 17:03:49 -07:00
Jerry
a9c868225e feat(compress): preserve recent N user messages during context compression
Add _ensure_last_n_user_messages_in_tail to guarantee the last N user
messages survive compression in the uncompressed tail, with surrounding
assistant/tool context preserved.

- Add min_tail_user_messages parameter (default 3) to ContextCompressor
- New _ensure_last_n_user_messages_in_tail method generalizes single-user protection
- Skip context-summary handoff banners when counting user messages
- User messages are clean boundaries — skip _align_boundary_backward
- Wire through cli.py, agent_init.py, and gateway cache busting keys

Config:
  compression:
    min_tail_user_messages: 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 17:03:49 -07:00
Teknium
69365109b3 fix(compression): mark raw skill_view bodies summarized away, not only pre-pruned rows
_collect_ghosted_skill_names() covers both ghost-skill shapes in the
compressed middle window: rows already demoted to a [SKILL_PRUNED: ...]
marker AND raw skill_view bodies (> _SKILL_VIEW_PRUNE_MIN_CHARS) that
survived Phase-1 inside an earlier protected tail and then aged into the
compression window — the summarizer paraphrases those instructions away
too. Shared threshold constant between the emit site and the scan.
Pinned by a live-probe-shaped test (real compress(), mocked aux LLM).
2026-07-23 16:58:06 -07:00
Teknium
28f73d32e9 test(compression): ghost-skill defense suite — marker round-trip, protected prune, real-compress survival
21 tests pinning the salvaged #44166 behavior:
- marker emit + extractor round trip (patterns adapted from PR #32375
  by @LeonSGP43, with credit)
- no-duplicate re-injection when the canonical marker survived (the
  original PR's presence-check defect)
- Phase-1 protection for just-loaded / user-referenced skills, and the
  Pass-4 pressure override that keeps #61932 fixed
- deterministic marker survival through a REAL compress() with a mocked
  aux LLM: drop → re-injected, keep → not duplicated, static-fallback
  path, iterative re-compression via rehydrated handoff
- markers never classify as handoff content (classify_summary_content /
  _strip_context_summary_handoff_message untouched)
- SKILLS_GUIDANCE Skill Safety Rule renders with real newlines
2026-07-23 16:58:06 -07:00
Teknium
44c67fca91 fix(compression): ghost-skill defense — canonical marker constant, protected-skill prune guard, deterministic marker survival
Salvage rework of PR #44166 (@dolphin-creator) onto current main:

- ONE canonical prune marker: _skill_pruned_marker(name) builds
  '[SKILL_PRUNED: ... reload with skill_view(name='X')]'; both emit
  sites and the survival presence check use the same string, fixing the
  original PR's defect where the emitted marker was '[SKILL_PRUNED:'
  but the presence check looked for '[SKILL_PRUNED]' (re-injection
  duplicated markers that had survived).
- Phase-1 prune (_prune_old_tool_results) now threads a protected-skill
  set: skills whose skill_view call is within the last 10 messages, in
  the protected tail, or named in a tail user message keep their full
  bodies. Pass-4 pressure demotion deliberately overrides the guard so
  the #61932 dead-end shape cannot return.
- P2 deterministic marker survival: skill names are extracted from the
  summarizer INPUT (and the previous summary) before the aux LLM call
  and any dropped canonical markers are re-injected afterward under a
  '## Pruned Skills' section — routed through _redact_compaction_text,
  appended to the summary body only (never in front of SUMMARY_PREFIX
  or scaffolding start-of-content markers; classify_summary_content is
  unaffected). Same treatment on the static fallback path, re-applied
  after its size cap since truncation cuts exactly where markers land.
- Summarizer prompt gains a '## Pruned Skills' copy-verbatim section.

Fixes #32106.
2026-07-23 16:58:06 -07:00
Jonathan Boisson
6816f2f02c fix(P1+P2): marker names skill_view(name='X') + DEDUP rule for repeated [SKILL_PRUNED] markers
Surgical reapply of the marker-alignment and dedup-guidance halves of
PR #44166 commits 52341f6ca3 / 3d8a31432d / ae07412e4b onto current main:
- the [SKILL_PRUNED: ...] marker embeds the exact reload call
  skill_view(name='<skill>') so the model can act without guessing
- SKILLS_GUIDANCE Skill Safety Rule gains rule 4 (DEDUP): after one
  reload, remaining markers for the same skill are historical artifacts

Fixes #32106 (part).
2026-07-23 16:58:06 -07:00
Jonathan Boisson
5faef80a43 fix: ghost skill P0/P1 mitigation - [SKILL_PRUNED] marker + safety rule 2026-07-23 16:58:06 -07:00
Teknium
9b868f6f67 chore: contributor email mappings for wen0531 and iniak 2026-07-23 16:55:41 -07:00
Teknium
df051c17cc fix(vertex): surface vertex in the /model picker — credential gate + curated model list
Community verification of #56688 (zmack12344321) found two follow-up gaps
that kept Vertex invisible in the /model menu even after registry
registration:

1. hermes_cli/model_switch.py: list_authenticated_providers() had a
   credential gate hard-coded to API keys (with an aws_sdk special case
   only) — add a vertex branch using has_vertex_credentials(), mirroring
   the aws_sdk shape.
2. hermes_cli/models.py: Vertex's OpenAI-compatible endpoint has no
   /models listing route, so without a curated _PROVIDER_MODELS entry the
   picker only ever showed the current model — add a Gemini curated list.

Follow-up to #56688.
2026-07-23 16:55:41 -07:00
srojk34
3ea35d6711 fix(vertex,moa): register vertex in PROVIDER_REGISTRY and HERMES_OVERLAYS
The Vertex AI provider (added same-day, commit c73e74386) was never added to
either of the two provider registries that agent/auxiliary_client.py and the
MoA slot-resolution chain depend on, breaking Vertex outside the main
conversation loop:

1. hermes_cli/auth.py::PROVIDER_REGISTRY had no "vertex" entry. The
   plugin-auto-extend loop that normally fills gaps explicitly skips
   non-api_key auth types (`if _pp.auth_type != "api_key": continue`), and
   Vertex was never hand-declared like "bedrock" is. Because
   resolve_provider_client() in agent/auxiliary_client.py gates everything
   on `pconfig = PROVIDER_REGISTRY.get(provider)` and returns (None, None)
   immediately when pconfig is None, its `elif pconfig.auth_type == "vertex"`
   branch was permanently dead code — every auxiliary Vertex call (vision,
   title generation, reflection, context compression, MoA reference/
   aggregator slots) failed outright, not just a MoA-specific edge case.

2. hermes_cli/providers.py::HERMES_OVERLAYS also had no "vertex" entry, so
   hermes_cli.providers.get_provider("vertex") returned None. This backs
   _preserve_provider_with_base_url() in agent/auxiliary_client.py, which a
   MoA slot's resolved (base_url, api_key) pair needs to keep its "vertex"
   identity instead of silently collapsing to "custom" — losing the
   identity _refresh_provider_credentials() needs to re-mint an expired
   OAuth2 token (~1h lifetime) on a 401, and permanently breaking every
   subsequent call in that MoA preset for the rest of the session.

Fix mirrors the existing "bedrock"/aws_sdk entries in both registries
exactly, plus adds a "vertex" branch to _refresh_provider_credentials() (it
had branches for openai-codex/nous/anthropic/xai-oauth but not vertex,
so a 401 fell through to `return False` without evicting the stale cached
client).

- hermes_cli/auth.py: hand-declared vertex ProviderConfig(auth_type="vertex")
  in PROVIDER_REGISTRY, matching bedrock's shape.
- hermes_cli/providers.py: vertex HermesOverlay(auth_type="vertex") in
  HERMES_OVERLAYS + "Google Vertex AI" label override.
- agent/auxiliary_client.py: vertex branch in _refresh_provider_credentials
  that re-mints the token via get_vertex_config() and evicts the stale
  cached client.
- 8 new regression tests across tests/hermes_cli/test_vertex_provider.py and
  tests/agent/test_auxiliary_client.py: registry membership, end-to-end
  resolve_provider_client("vertex", ...) building a working client (proving
  the previously-dead branch is now reachable), and the 401-refresh/cache-
  eviction path.
2026-07-23 16:55:41 -07:00
iniak
a7d78ad685 fix: filter invalid MoA slot providers 2026-07-23 16:55:41 -07:00
liuhao1024
d4c6ae7b11 fix(moa): preserve save_traces/trace_dir on GUI config save
MoaConfigPayload does not declare save_traces or trace_dir, so
set_moa_models() overwrites cfg["moa"] with a dict that lacks these
hand-edited keys.  Use dict.update() to merge instead of replace.

Fixes #58819
2026-07-23 16:55:41 -07:00
Teknium
85b2d52b71 test(moa): regression tests for JSON-string reference_models parsing (follow-up to #59497) 2026-07-23 16:55:41 -07:00
刘文
3638abfbf9 fix(moa): parse JSON string reference_models in _normalize_preset
When reference_models is stored as a JSON string (e.g. from hermes moa
configure or hand-edited config.yaml), _normalize_preset silently
falls back to hardcoded defaults because the string fails both
isinstance(x, list) and isinstance(x, dict) checks.

Add json.loads() parsing before the type checks so both formats work.
2026-07-23 16:55:41 -07:00
liuhao1024
c1f5f0f911 fix(doctor): recognise 'moa' as a valid internal provider
MoA (Mixture of Agents) is a legitimate internal provider used by
Diagnosis presets and multi-model aggregation. When a MoA preset sets
model.provider to 'moa', hermes doctor incorrectly reports it as
'unrecognised' and suggests changing it, which would break the MoA setup.

Add 'moa' to the known_providers set alongside 'openrouter', 'custom',
and 'auto' so doctor recognises it as valid.

Fixes #58759
2026-07-23 16:55:41 -07:00
0xDevNinja
d661886c90 test(cli): assert HermesCLI.__init__ wires moa:<preset> to the moa provider
The existing tests cover _normalize_moa_model() in isolation and a local
precedence expression, but not the __init__ wiring itself. Add two
init-level regression tests: constructing HermesCLI(model='moa:strategy')
strips the prefix to model='strategy' and forces requested_provider='moa',
and the moa: prefix wins over an explicit --provider. Both fail if the
override is dropped from the requested_provider resolution.

Refs #56828
2026-07-23 16:55:41 -07:00
0xDevNinja
8d72845399 fix(cli): resolve moa:<preset> model in non-interactive mode
hermes chat -Q -m moa:strategy failed with 'model moa:strategy is not
supported' (HTTP 401/400): the raw model string was passed straight to
the real provider. The MoA virtual provider only got wired up through the
interactive /moa command and the model picker, never through the -Q
one-shot startup path.

resolve_runtime_provider already handles requested_provider == 'moa', and
agent_init builds the MoAClient off provider == 'moa' (surface-agnostic).
The only gap was mapping the moa:<preset> model string to that provider.

Add _normalize_moa_model() and apply it in HermesCLI.__init__ before
provider resolution: a moa:<preset> model sets requested_provider='moa'
and model=<preset>, so the existing MoA path runs in non-interactive mode
too. The moa: prefix wins over an explicit --provider (previously
--provider deepseek -m moa:strategy silently dropped MoA).

Fixes #56828
2026-07-23 16:55:41 -07:00
Teknium
b7a05b6b6f fix: re-anchor summary-input bound to current main + bound iterative path
Follow-ups on top of the cherry-picked #27748 mechanism:
- move the cap constant to module level with full rationale comment
  (class attribute aliases it so subclasses/tests can override)
- bound the iterative-update path too: the PREVIOUS SUMMARY block is
  passed through _bound_summary_input so a pathological rehydrated
  handoff cannot blow up the prompt (previous summary + new turns each
  capped)
- extra regression tests: byte-identical small-input passthrough
  (identity), direct bound+marker unit check, bound-after-per-message-
  truncation shape (hundreds of under-_CONTENT_MAX turns), iterative
  path bounded, marker vs classify_summary_content non-collision
- contributor email mapping for @robgfl45
2026-07-23 16:44:53 -07:00
Cluster2
80ece3867b fix: bound compression summary input 2026-07-23 16:44:53 -07:00
Teknium
fa4800414c feat(compression): prompt-cache reclaim gate + hardened wiring for proactive prune
Follow-ups on top of the cherry-picked #62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  #62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
2026-07-23 16:44:12 -07:00
Kolektori
cb481e2f2b feat(compression): proactive tool-result pruning for large-window models
The phase-1 tool-result prune only runs inside compress(), which fires
near 50% of the context window, so it never triggers on large-window
models; old tool outputs then ride in history and are re-sent every turn.

Add prune_tool_results_only(): the same no-LLM prune on a separate, low
proactive_prune_tokens trigger, run as an elif to the compression branch.
Opt-in (default 0), protects the recent tail by message count.

Add the method to the ContextEngine base as a no-op default so pluggable
engines inherit it safely (the post-tool-call path never AttributeErrors on
a non-built-in engine); the built-in compressor supplies the real prune.
Register both keys under the top-level compression config with defaults and
document them.
2026-07-23 16:44:12 -07:00
Teknium
66fdcfa3bd fix: harden salvaged preflight display rollback for test-double density
Follow-up to the #54805 cherry-pick (skill guard rules):
- turn_context.py: snapshot_preflight_display_tokens gets a
  getattr+callable guard (SimpleNamespace compressor doubles / plugin
  context engines lack the ContextCompressor-only method) and the
  snapshot value is type-pinned to a real int (bool excluded) before
  arming the rollback — MagicMock compressors return truthy Mocks.
- turn_finalizer.py: interrupted pinned 'is True', the
  _turn_received_provider_response read pinned 'is not True' (MagicMock
  auto-attrs are truthy), and the compressor rollback method call gets a
  getattr+callable guard. Rollback stays display-only: it never touches
  _ineffective_compression_count or any durable guard, and preserves the
  -1 post-compaction sentinel.
2026-07-23 16:38:06 -07:00
izumi0uu
17a81ac89e fix(context_compression): roll back interrupted preflight state pollution
Interrupted turns can seed a speculative display token count before the provider receives the request. Restore that display-only seed when interruption wins the race, while preserving completed post-compaction state and treating a successful provider response independently of optional usage metadata.

Constraint: #54776 remains reproducible on current main, while review #4702305384 identifies anti-thrashing rollback as stale and usage receipt as an unreliable response-completion signal.
Rejected: Restore anti-thrashing counters from a preflight snapshot | current main derives their verdict from real provider usage after a completed compaction boundary.
Confidence: high
Scope-risk: narrow
Directive: Keep interrupted preflight rollback display-only, and never infer provider completion from the presence of usage metadata.
Tested: ./.venv/bin/python -m pytest -q tests/run_agent/test_413_compression.py (29 passed); turn-finalizer/conversation-loop tests (31 passed); context-compressor targeted tests (12 passed); infinite-compaction targeted tests (3 passed); ruff; git diff --check.
Not-tested: End-to-end interactive interrupt through CLI or gateway transport.
2026-07-23 16:38:06 -07:00
Teknium
6a8d31856f chore: map kinsonnee@gmail.com -> WOLIKIMCHENG for #59526 salvage 2026-07-23 16:27:06 -07:00
root
34678d2f2e fix(compression): skip empty post-handoff summary windows 2026-07-23 16:27:06 -07:00
Teknium
eebc2286fc fix(gateway): retry-next-message semantics for compression_deferred + regression suite
Gateway half of the #49874 salvage: pass compression_deferred through
both _run_agent_inner result dicts and guard the compression-exhausted
auto-reset block with it — a lock-contended defer keeps the session
intact (the concurrent compressor is actively shrinking it) instead of
wiping it via reset_session.

Regression tests:
- tests/run_agent/test_compression_lock_defer.py — provider-mock 413 and
  400-overflow turns whose compression pass lost the lock end as
  compression_deferred (failed=False, no compression_exhausted); flag
  unset keeps the terminal exhaustion path byte-identical; type-pin
  tests vs MagicMock agents and junk flag values; cap=1 e2e proving the
  refunded pre-API defer leaves the budget for the provider-proven
  413 retry.
- tests/agent/test_preflight_lock_defer.py — a lock-skipped preflight
  pass stops the loop WITHOUT arming preflight_compression_blocked;
  plain no-op still arms it; MagicMock junk does not defer.
- tests/gateway/test_compression_deferred_soft_result.py — AST pin that
  the deferred branch guards the auto-reset chain and performs no
  session mutation (mirrors test_35809_auto_reset_clean_context.py).
2026-07-23 16:23:57 -07:00
helix4u
056a40aa4d fix(agent): defer turns during compression lock contention instead of exhausting
A lock-loser compression pass returns its input unchanged, which the
automatic compression sites misread as 'cannot compress further': the
preflight loop armed the insufficient-progress blocker, the pre-API gate
burned a shared attempt, and a lock-contended 413/overflow retried into
the attempt cap and returned compression_exhausted — which the gateway
answers with a full session auto-reset (#9893/#35809). A temporary
concurrent-compression defer wiped the session.

Consume the landed #69870 lock-skip signal on every automatic path
(preflight in turn_context, pre-API pressure gate, 413 handler, overflow
handler, post-tool compaction): when a pass no-ops AND the type-pinned
lock-skip flag is set, refund the attempt (never count it toward the cap
or the insufficient-progress blocker), and when the turn cannot proceed
(provider already proved the request does not fit) end it with a soft
compression_deferred result — distinct from compression_exhausted — so
the gateway keeps the session intact and the next message retries after
the concurrent compressor finishes.

The new compression_skipped_due_to_lock() reader is type-pinned
(is True or isinstance(str)) per the MagicMock auto-attribute rule, and
compress_context() now also clears the signal at the very top of every
attempt (per-attempt state rule, #58629/#69853) so a stale value can
never make a later breaker/codex no-op look like lock contention.

Salvaged from PR #49874; rebuilt on main's #69870
_compression_skipped_due_to_lock signal instead of the PR's parallel
_compression_deferred_by_lock triple.
2026-07-23 16:23:57 -07:00
Teknium
fdefb2d38c fix(compression): prefer psutil.pid_exists for lease liveness probe; add same-pid self-reclaim guard
Hardening on top of the salvaged dead-PID lease reclamation from PR #65775
(@the3asic):

- Probe via psutil.pid_exists (hard dependency; CONTRIBUTING.md critical
  rule #1) with the contributor's os.kill(pid, 0) POSIX probe retained
  only as a scaffold-phase fallback when psutil is missing.
- Same-process holders (pid == os.getpid()) are never probed and never
  self-reclaimed — another thread's live lease is owned by the lease
  refresher/release path.
- Any probe doubt (exceptions, permission errors) conservatively keeps
  the lease until normal TTL expiry; Windows stays TTL-only.
- Tests: psutil-first dead-pid reclaim (probe call pinned), os.kill
  fallback path, probe-doubt keeps lease, same-pid no self-reclaim,
  legacy holder + Windows paths assert NO probe via either API.
2026-07-23 16:21:19 -07:00
3ASiC
6ab8428b88 fix(compression): keep PID probing POSIX-only 2026-07-23 16:21:19 -07:00
3ASiC
8cd49c496f fix(compression): reclaim locks from dead processes 2026-07-23 16:21:19 -07:00
golldyck
69339aab91 fix(compaction): skip compression when it can't reduce tokens
compress_trajectory (and _async) replaced the compressible middle region with
a [CONTEXT SUMMARY] turn without checking that the region is actually larger
than the summary. When a large protected system prompt dominates the budget,
the compressible middle can be tiny; replacing e.g. a 2-token middle with a
~60-token summary GROWS the trajectory (tokens_saved negative), marks it
was_compressed, and still spends a summarization call — the opposite of the
intent, on exactly the hard over-budget cases.

Add a net-savings guard mirroring the code's own comment (net_savings =
region_tokens - summary_target_tokens): if the safely-compressible region is
no larger than summary_target_tokens, return the trajectory unchanged. Applied
to both the sync and async paths. Add sync+async regression tests.
2026-07-23 16:21:03 -07:00
Rain
bc7212cf93 feat(moa): per-reference-model max_tokens override
MoA reference_max_tokens is preset-level — one cap for all reference
models. When mixing a verbose model with a terse one, a single cap is
either too tight for the terse model or too loose for the verbose one.

Now each reference slot can optionally carry its own max_tokens:

  reference_models:
    - provider: openrouter
      model: deepseek/deepseek-v4-pro
      max_tokens: ***        # per-slot cap, overrides preset-level
    - provider: openai-codex
      model: gpt-5.5
      # no max_tokens → falls back to preset-level reference_max_tokens

_clean_slot (moa_config.py) preserves an optional max_tokens field on
the slot dict, coerced via _coerce_int_or_none. _run_reference
(moa_loop.py) reads slot-level max_tokens first, falling back to the
preset-level cap passed by the caller. Slots without the field are
unaffected — backward compatible.

Type hints on slot-handling functions updated from dict[str, str] to
dict[str, Any] to reflect the now-heterogeneous slot shape.
2026-07-23 16:17:27 -07:00
aui
ead9d7b256 test: cover gemini-native max_tokens forwarding in _build_call_kwargs
Requested in review: builder-level assertions that the gemini-native
branch forwards max_tokens (provider names and the native
generativelanguage.googleapis.com base_url, max_tokens=600), plus a
control showing gemini models on OpenAI-compatible endpoints — including
Gemini's own /openai compatibility endpoint — keep the existing omission
behavior (#34530).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:17:27 -07:00
aui
4ee74fa5df fix: forward max_tokens to gemini-native so MoA reference cap applies
_build_call_kwargs omitted max_tokens for every provider except
anthropic-compat endpoints and NVIDIA NIM. Gemini's native
generateContent maps max_tokens -> maxOutputTokens and, when it is
omitted, applies a fixed 65,535-token ceiling (not "the model's full
budget"), so dropping the value made MoA's reference_max_tokens a
silent no-op for gemini advisors — they ran effectively uncapped
(observed ~2900 output tokens against a configured cap of 600),
inflating per-turn MoA latency.

Forward max_tokens for the gemini-native path (provider name or native
base_url). Gemini supports maxOutputTokens, so the cap is safe here;
providers that reject max_tokens (Copilot, GPT-5 max_completion_tokens,
ZAI vision) are unaffected — they still omit it as before.
2026-07-23 16:17:27 -07:00
Janig88
3dce1b967f fix(auxiliary): scope max_tokens to moa_reference only (not aggregator)
Per review feedback from teknium1: reference_max_tokens is an advisors-only
contract. The aggregator is the acting model and must not be capped by the
reference budget. Changed _is_moa from startswith('moa_') to exact match on
'moa_reference'. Added regression test proving aggregator does NOT receive
max_tokens.
2026-07-23 16:17:27 -07:00
Janig88
289fad1868 fix(auxiliary): thread task=task through _build_call_kwargs in fallback helpers 2026-07-23 16:17:27 -07:00
Janig88
3616ce006a fix: use auxiliary_max_tokens_param for Copilot GPT-5 compat
Copilot review pointed out that hardcoding kwargs['max_tokens'] would
400 on models requiring max_completion_tokens (GPT-5 family, Copilot).
The existing auxiliary_max_tokens_param() helper already selects the
correct parameter name per model — use it instead of hardcoding.

Test updated to parametrize expected_key so the Copilot gpt-5.5 case
correctly asserts max_completion_tokens instead of max_tokens.

Addresses Copilot review comments on both files.
2026-07-23 16:17:27 -07:00
Janig88
32a4faa2d5 fix(auxiliary): honor max_tokens for MoA reference/aggregator tasks
PR #56756 added reference_max_tokens to cap MoA advisor output and cut
turn latency. The value is correctly threaded through five layers of MoA
code (moa_config → conversation_loop → aggregate_moa_context →
_run_references_parallel → _run_reference → call_llm(task='moa_reference',
max_tokens=800, ...)).

However, _build_call_kwargs() in auxiliary_client.py silently drops
max_tokens for all OpenAI-compatible providers (PR #34845, which fixed
endpoints and NVIDIA NIM keep it. This means reference_max_tokens never
reached the API for the vast majority of providers.

The bug affects every OpenAI-compatible MoA reference/aggregator slot:
Z.AI (coding plan), OpenRouter, OpenAI, GitHub Copilot, and local
providers. Only Anthropic-compat endpoints (MiniMax, /anthropic URLs)
worked — by coincidence, not MoA-aware design.

Fix: thread the 'task' parameter through all six _build_call_kwargs()
call sites. When task starts with 'moa_', max_tokens is always included
in the request kwargs regardless of provider. Non-MoA auxiliary tasks
(compression, titles, vision, etc.) keep PR #34845 behavior unchanged.

Verified end-to-end:
- Z.AI GLM-5.2 with max_tokens=50 → returned exactly 50 tokens
- Z.AI GLM-5.2 with max_tokens=20 → returned exactly 20 tokens
- Z.AI GLM-5.2 uncapped → returned 315 tokens
- 7 new regression tests covering 4 providers, Anthropic wire, non-MoA
  tasks, and prefix-matching boundary
- 288 auxiliary_client tests pass (was 281, +7 new), 84 MoA tests pass
- Zero regressions
2026-07-23 16:17:27 -07:00