Commit graph

15574 commits

Author SHA1 Message Date
Burgunthy
9166d727b8 fix(profile-routing): remove dead forum cache, warn on missing profile
Three follow-ups to the initial routing PR after code review:

1. Remove dead forum-post hierarchy cache. The `_forum_post_cache`,
   `register_forum_post()`, and `resolve_forum_channel()` were never
   wired up — no caller in the codebase. Discord's adapter already
   sets `parent_chat_id` to the immediate parent (forum channel for a
   forum post), so the existing `self.chat_id == parent_chat_id`
   branch in `matches()` handles forum posts correctly without a
   cache. The hierarchical-resolution branch in `matches()` and the
   bounded-LRU infrastructure are removed.

2. Fix docstring specificity numbers (8 → 14, 4 → 6) and rewrite the
   "Hierarchical matching" section to describe the actual one-level
   parent_chat_id behavior. Removed unused `OrderedDict` and `Set`
   imports.

3. Warn loudly when a routed profile doesn't exist on disk. Previously,
   a typo in `profile_routes` (e.g. `crypto-tradr`) silently fell back
   to the global HERMES_HOME, causing the message to read the default
   profile's memory/credentials with no signal to the operator. Now
   emits a `logger.warning` with the profile name, source identifier,
   and the fallback reason. Bare-exception path also gets `exc_info`.

Tests:
- test_profile_routing.py: +2 tests verifying forum post matching via
  direct parent_chat_id (covers the case the removed cache was meant
  for). 31 total, all pass.
- test_profile_resolution.py: NEW, 12 tests covering resolution order
  (source.profile > routing > active > default), missing-profile
  warning, exception handling, and routing consultation. All pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
5e65f6d79f feat(gateway): add profile-based routing for inbound messages
Adds gateway.profile_routes config that routes specific Discord
guilds/channels/threads (and other platforms) to different profiles.
The routing engine uses hierarchical specificity matching
(thread > channel > guild) with bounded LRU caching for forum post
resolution.

Routing result is stamped on source.profile by BasePlatformAdapter
.build_source() at inbound time. When gateway.multiplex_profiles is on,
the existing _profile_runtime_scope machinery picks up source.profile
and runs the whole turn inside the profile's HERMES_HOME — so memory,
skills, config, and secrets all resolve to that profile automatically.
No new isolation code is added; this PR only adds the routing decision
layer on top of the existing multiplexing infrastructure.

Configuration:

    gateway:
      multiplex_profiles: true
      profile_routes:
        - name: server-default
          platform: discord
          guild_id: "GUILD_ID"
          profile: server-profile
        - name: special-channel
          platform: discord
          guild_id: "GUILD_ID"
          chat_id: "CHANNEL_ID"
          profile: channel-profile

When multiplex_profiles is off, profile_routes is ignored (no behavior
change for single-profile gateways).

Tests: 29 unit tests covering specificity scoring, hierarchical
matching, path-traversal validation, and config parsing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Teknium
6efec39ecf
refactor(skills): rework blender-mcp skill around the catalog MCP entry (#65066)
The optional blender-mcp skill predates the blender MCP catalog entry
(#64463) and taught the agent to hand-roll raw TCP JSON to the addon's
socket on port 9876 from execute_code — bypassing the catalog's version
pinning and install-time tool curation.

Reworked to v2.0.0 as the companion skill for the catalog entry:
- prerequisites now go through 'hermes mcp install blender'
- interaction surface is the four curated MCP tools, not a raw socket
- keeps the valuable content: addon setup, bpy recipes (materials,
  keyframes, render-to-file), pitfalls (timeouts, absolute paths,
  object mode), plus new pitfalls (xvfb headless, no-sandbox warning,
  remote-host path resolution)
- explicit anti-pattern note: do not hand-roll TCP to 9876
- description shortened to <=60 chars per skill authoring standards

alireza78a's original bpy patterns and pitfalls are preserved and
credited. Docs page regenerated via generate-skill-docs.py (scoped to
this skill only; unrelated generator drift left untouched).
2026-07-15 09:49:42 -07:00
liuhao1024
6a8e7069b4 fix(agent): dedup codex incomplete interims on visible content
Two consecutive incomplete assistant interims with identical visible
content (content + reasoning) are collapsed even when opaque provider
state (encrypted reasoning item ids, message item phases) drifts per
continuation — previously that drift defeated dedup and caused message
storms (#52711). The latest opaque payload is written onto the existing
message in place, so continuation replay still uses fresh provider
state.

Salvage note: the original PR also made the 3-retry continuation cap
cumulative per turn (no reset on progress). That half is intentionally
dropped — a legitimate long turn alternating incomplete/progress would
hard-fail at 3 cumulative, changing semantics beyond the reported bug.
2026-07-15 09:49:11 -07:00
Teknium
09b6d22dfe fix(gateway): harden hidden-incomplete detection against the sentinel final_response
Follow-up to the salvaged #51657: the conversation loop returns the
retry-exhaustion sentinel as BOTH final_response and error, so the
original detector (which required final_response to be falsy) never
fired on real exhaustion turns — the sentinel text was delivered
verbatim into the channel, exactly the #51628 poisoning vector. Detect
the sentinel echo, blank it before empty-response normalization, and
never suppress a turn whose final_response is genuine model text.
Also: dedupe-guard mock fix in the test fixture (has_platform_message_id
must return False, not a truthy MagicMock) and two guard tests
(real answer never suppressed; interrupted/failed never classified).
2026-07-15 09:48:24 -07:00
LeonSGP43
08015c3a8f fix(gateway): suppress hidden-only incomplete codex turns 2026-07-15 09:48:24 -07:00
Sk
fe1ab949fd fix(agent): treat Codex incomplete content filter as refusal
Map Codex Responses status=incomplete with incomplete_details.reason=content_filter to finish_reason=content_filter so the existing refusal/fallback path runs instead of burning incomplete continuation attempts.
2026-07-15 09:47:37 -07:00
Teknium
704bbcca80 feat(compressor): keep image URLs and unknown-part markers in summarizer serialization
Follow-up on @AlexFucuson9's cherry-picked multimodal flattening:

- http(s) image parts render as '[image: <url>]' so the summary keeps a
  referenceable handle after compaction (base64 data: URLs still
  collapse to '[image]' — no reusable reference, and leaking them is
  the bug being fixed)
- unknown part types (document, future shapes) render as '[<type>]'
  instead of being silently dropped
- test docstrings corrected: the original crash premise is stale
  (redact_sensitive_text grew str() coercion in #52147); the live bug
  is repr-noise + base64 leakage into the summarizer input
2026-07-15 09:47:04 -07:00
AlexFucuson9
868a2f7d17 fix: handle list content in _serialize_for_summary for multimodal messages
msg.get('content') can return a list of parts for multimodal messages
(containing text, images, etc.). The old code passed this list directly
to redact_sensitive_text(text: str), which raised AttributeError on
list.replace(), causing context compression to fail entirely for any
session with attached images.

Fix: detect list content and extract text parts before redacting.
Image parts are replaced with '[image]' placeholder.
2026-07-15 09:47:04 -07:00
ethernet
1600008ab0 fix(desktop): show +/- summary on collapsed review folders
ReviewDirRow never rendered a DiffCount, so collapsed folders gave no
indication of the additions/deletions inside them. The tree builder
already aggregates added/removed onto directory nodes (verified by
tree-data tests) — this just renders them, matching the file rows.
2026-07-15 11:57:33 -04:00
ethernet
1d48863b85 fix(desktop): inline git identity in worktree tracking test for CI
CI runners have no global git identity. The remote repo seed commit
needs inline -c user.email/user.name flags, matching the pattern
already used by ensureGitRepo.
2026-07-15 11:46:29 -04:00
ethernet
678b86df1c fix(desktop): worktree from origin/main should not set up upstream tracking
When "new worktree" branches off a remote-tracking ref like origin/main,
`git worktree add -b <branch> <dir> origin/main` auto-sets upstream
tracking (branch → origin/main), producing `branch:origin/main` in branch
listings. The user wants a standalone local branch — like `git checkout
origin/main && git checkout -b branch` — not one silently wired to the
remote. Add `--no-track` when the base is an `origin/` ref. Local branch
bases are unaffected (they never triggered tracking).
2026-07-15 11:46:29 -04:00
Teknium
07be37d996 fix(auxiliary_client): warn once + regression tests for bootstrap version skew
Follow-up on the salvaged fallback: silent degradation is how #64333 went
unnoticed (jobs dead on arrival, only errors.log knew). Warn once with a
resync hint, and cover both the skewed and healthy paths with tests.
2026-07-15 07:47:18 -07:00
liuhao1024
f3ec79964e fix(auxiliary_client): add backward compatibility for build_keepalive_http_client import (#64333) 2026-07-15 07:47:18 -07:00
HexLab98
2388e0687b test(input): preservation regressions and prompt.submit boundary (#62557)
Add cases for mid-string markers, trailing punctuation, and insufficient
tail repeats; verify prompt.submit passes sanitized text to run_conversation.
2026-07-15 07:39:42 -07:00
HexLab98
1011cd24e2 fix(input): strip bracketed-paste leaks before prompt persistence (#62557)
Extract shared hermes_cli/input_sanitize.py (bracketed-paste wrapper stripping
and terminal ~[[e artifact suffix collapse) and wire it into prompt.submit
and the Desktop composer so corrupted user text is cleaned before
messages.content is persisted.
2026-07-15 07:39:42 -07:00
Teknium
7c954969b7
fix(auxiliary): route direct-create aux callers through call_llm (#65029)
* fix(auxiliary): route direct-create aux callers through call_llm (#35566)

Five callers (kanban_decompose, kanban_specify, profile_describer, and
goals.py's judge + draft-contract) built raw clients via
get_text_auxiliary_client() and passed extra_body=get_auxiliary_extra_body()
— which only returns Nous portal tags and ignores
auxiliary.<task>.extra_body from config.yaml entirely. That was the
remaining half of #35566 after the call_llm path was fixed.

Routing them through call_llm(task=...) gives each caller the full
auxiliary contract for free: task extra_body, the reasoning_effort
shorthand, transient retries, provider-profile projection, and fallback
chains. goal_judge gains a DEFAULT_CONFIG block (it had none — its
provider/model overrides silently didn't exist as documented keys).

get_auxiliary_extra_body() now has zero non-test callers; kept for
plugin back-compat.

Fixes #35566.

* test: migrate kanban dashboard + CLI specify mocks to call_llm

Two more consumers of specify_task mocked the old
get_text_auxiliary_client symbol (missed in the first sibling sweep —
they live outside tests/hermes_cli's kanban files): the dashboard
plugin's /specify endpoint tests and the /kanban slash-command E2E.
Same migration as the rest: mock call_llm at the source, no-provider
now surfaces via the LLM-error branch.
2026-07-15 07:39:17 -07:00
Teknium
306e2d2318 chore: AUTHOR_MAP entry for Epoxidex (PR #29820 salvage)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
2026-07-15 06:38:28 -07:00
Epoxidex
8662254ab2 fix(ollama): emit top-level reasoning_effort=none on /v1/chat/completions (#25758)
Ollama's /v1/chat/completions silently ignores extra_body.think (it only
honours it on /api/chat — ollama/ollama#14820), so agent.reasoning_effort:
none never actually disabled thinking on OpenAI-compatible Ollama routes.
Emit the top-level reasoning_effort='none' field (which Ollama respects)
alongside think=False (kept for proxies and the native /api/chat path).

The PR's second half (propagating reasoning_config to the background-review
fork) already landed on main via agent/background_review.py, so only the
provider-profile change is salvaged here, resolved onto the current
GLM/effort-aware profile.

Salvaged from PR #29820 by @Epoxidex.
2026-07-15 06:38:28 -07:00
Teknium
a7ef17da7a chore(release): map dorokuma in AUTHOR_MAP 2026-07-15 06:25:10 -07:00
Teknium
31dcd68bfa test: cover Anthropic aux extra_body passthrough (Bug B scope + exclusions)
Five tests for the salvaged #37217 Bug B fix: vendor-field passthrough,
reasoning-key + private-key exclusion, merge-over-existing (fast-mode
speed), no-extra_body regression guard, reasoning-only adds nothing.

Live probes against api.anthropic.com informed the exclusion design:
Anthropic strictly validates the request body (unknown keys 400 with
'Extra inputs are not permitted'), so the passthrough forwards only
caller-configured fields and never the OpenAI-shaped reasoning dict
(translated natively) or _-private plumbing keys.
2026-07-15 06:25:10 -07:00
dorokuma
771571aee4 fix(auxiliary): pass reasoning_config and extra_body through to auxiliary Anthropic calls
Two related bugs in _AnthropicCompletionsAdapter.create() in
agent/auxiliary_client.py silently discard caller-supplied
reasoning_config and extra_body on the Anthropic-Messages
auxiliary-protocol path:

  * Bug A: reasoning_config=None was hardcoded at L1000, so the
    reasoning_config parameter on build_anthropic_kwargs was
    unreachable for any auxiliary task. The main agent path
    (agent/transports/anthropic.py) already reads
    reasoning_config from caller params; this PR aligns the
    auxiliary adapter with the same pattern.

  * Bug B: create(**kwargs) accepts an OpenAI-style kwargs
    payload from the caller but only forwards a hand-picked
    subset to self._client.messages.create(). Any caller-supplied
    extra_body (e.g. thinking control, metadata, service_tier,
    vendor-specific fields) was dropped on the floor. The
    codex/responses transport in the same file already merges
    extra_body; the Anthropic branch is the gap.

This unlocks the caller-supplied extra_body path so auxiliary
callers can set per-vendor request fields (including
thinking: {type: "disabled"} for Anthropic-compatible vendors
that require an explicit disable on the wire), and lets the
reasoning_config kwarg flow into build_anthropic_kwargs like the
main agent does. Both changes are backward-compatible for
callers that don't pass the affected kwargs.

Affected providers (all routed through _AnthropicCompletionsAdapter
via _maybe_wrap_anthropic): anthropic (native), minimax /
minimax-cn, kimi-coding / kimi-coding-cn, z.ai / GLM, and any
custom /anthropic-suffixed endpoint. See PR description for
related issues (#35566, #7209, #16533, #32813, #29248).
2026-07-15 06:25:10 -07:00
Teknium
9df5f879b4 feat(mcp): enforce exact version pins across the whole MCP catalog
Catalog entries now follow the same supply-chain rules as pyproject
dependencies:

- n8n: install.ref main -> full commit SHA 7a9ae007 (2026-05-23,
  branches/tags can be moved by the upstream owner; SHAs cannot)
- new contract test: every shipped manifest must pin exactly —
  git installs need a 40-char SHA, uvx/npx-style launchers need
  pkg==X / pkg@X with a digit-leading version (rejects bare names,
  ranges, and npm dist-tags like @latest)
- module docstring documents the pin policy (exact version, 2-week
  cooldown)

unreal-engine and linear are http transports (server runs elsewhere)
so there is nothing to pin at the transport layer.

Verified: unpinning blender-mcp in the manifest makes the contract
test fail with a named diagnostic; restoring the pin passes.
2026-07-15 04:56:25 -07:00
Teknium
a52393a3b6 fix(mcp): pin blender-mcp to 1.6.4 per catalog dependency policy
MCP catalog entries follow the same supply-chain rules as pyproject
dependencies: exact version pin, and the pinned release must be at
least 2 weeks old. blender-mcp 1.6.4 released 2026-06-11 (~5 weeks
old, also the latest release). uvx now resolves the exact version
instead of latest-at-launch.
2026-07-15 04:56:25 -07:00
Teknium
9be941dac1 feat(mcp): add Blender to the MCP catalog with a curated 4-tool default
Adds optional-mcps/blender (ahujasid/blender-mcp, stdio via uvx). The
server advertises 22 tools; 18 front optional asset services with no
upstream trim mechanism, so tools.default_enabled pins the install to
the core surface (scene/object info, viewport screenshot, code exec)
and the rest stay opt-in through 'hermes mcp configure blender'.

Manifests can now declare transport.env (static, non-secret subprocess
env vars), parsed/validated in _parse_manifest and written by
_build_server_config — used here to ship DISABLE_TELEMETRY=true per
the no-telemetry-without-opt-in policy. Runtime already honored
per-server env; manifests just couldn't declare it.
2026-07-15 04:56:25 -07:00
Teknium
7af59f474d fix(codex): raise hard-ceiling default above the max stale floor
With the TTFB watchdog now scaled (not disabled) for large requests on
main, the hard ceiling is a backstop against mid-stream wedges, not the
primary stall detector. The original 600s default clamped BELOW the
intentional 1200s stale floor for >100k-token requests, partially
reverting the floor that keeps healthy gateway-scale payloads alive.
Raise the default to 1500s so the ceiling only catches unbounded
growth, never healthy slow turns.
2026-07-15 04:55:48 -07:00
Ahmett101
bcd7e2ce89 fix(agent): add finite hard ceiling on openai-codex request time (#64507)
A large Codex request (estimated context >= 10k tokens) disables the no-byte
TTFB watchdog on purpose, and openai_codex_stale_timeout_floor *raises* the
stale timeout (up to 1200s at >100k tokens) so healthy gateway-scale payloads
aren't aborted mid-prefill. When the backend genuinely stalls — no first byte
AND no events, exactly the #64507 symptom — the request is only reclaimed at
that high stale floor, so the session can hang for 13+ minutes with an idle
slash_worker and no ended_at/end_reason while Desktop still shows it as active.

Add a flat, finite hard ceiling on total openai-codex request time that always
applies (min() of the computed stale timeout and the ceiling) regardless of the
TTFB-disable / stale-floor interaction. A stalled large request is now killed
at the ceiling and the retry loop / visible failure path takes over instead of
hanging indefinitely. Tunable via HERMES_CODEX_HARD_TIMEOUT_SECONDS (default
600s; 0 disables the ceiling to restore pre-fix behavior).

Closes #64507
2026-07-15 04:55:48 -07:00
liuhao1024
c2a3b9ce58 fix(state): use PASSIVE checkpoint for periodic WAL flush to prevent B-tree corruption
TRUNCATE checkpoint every 50 writes causes B-tree corruption on large
databases (65K+ pages) due to the exclusive-lock I/O pressure from
checkpointing thousands of frames at once.

Switch periodic _try_wal_checkpoint() to PASSIVE mode which does not
require an exclusive lock and cannot corrupt pages under I/O pressure.
Keep TRUNCATE in close() and pre-VACUUM paths where it is safe
(infrequent, controlled conditions).

Also replace silent `except Exception: pass` with logged warnings for
checkpoint failures so operators can detect early corruption signals.

Fixes #45383
2026-07-15 04:55:46 -07:00
Frowtek
779c0dd80d fix(compressor): unwrap web_extract dict URLs in tool-result summaries
_summarize_tool_result() built the web_extract summary straight from the
first `urls` entry. When web_search results are forwarded into web_extract
(a common chain), that entry is a dict ({"url"/"href": ...}) rather than a
URL string. With 2+ URLs the `url_desc += " (+N more)"` step then raised
`TypeError: unsupported operand type(s) for +=: 'dict' and 'str'`, aborting
the pre-compression pruning pass; with a single URL the raw dict repr
leaked into the summary text.

Unwrap the URL from dict entries before use — mirroring the existing
web_extract handling in agent/display.py (`_display_url`),
tools/web_tools.py (`_web_extract_url`) and acp_adapter/tools.py
(`build_tool_title`) — so `url_desc` is always a string and the
concatenation is always str + str.

Adds regression tests covering multiple/single dict URLs, the href key,
malformed dicts, and the plain-string path.
2026-07-15 04:55:36 -07:00
Teknium
8fa8aabbbb
test(codex): pin codex_backend issuer in xai-scoped salvage test (#64844)
test_normalize_codex_response_salvage_is_xai_scoped broke on main when
two same-day merges crossed: #64764 (#64434 — trust response.status for
reasoning-only turns on UNRECOGNIZED Responses backends) changed what a
bare _normalize_codex_response(response) call returns for
status='completed' reasoning-only output (now 'stop'), while #64768
added this test calling with no issuer_kind and expecting 'incomplete'.

The test's intent is that the xAI reasoning-channel salvage does not
leak into other special-cased backends — pin issuer_kind='codex_backend'
so it exercises exactly that (same pattern as
test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete,
which was already pinned for #64434).
2026-07-15 04:27:32 -07:00
HexLab98
1b059d1ae7 test(config): regression for merge_existing partial saves (#62723) 2026-07-15 04:26:20 -07:00
HexLab98
0ab90040ad fix(config): preserve platforms on partial save_config writes (#62723)
Add merge_existing to save_config (default False for full-document callers
like the dashboard YAML editor) and route partial writes through
_merge_partial_save. _persist_migration writes the full migrated dict
directly so deleted keys are not resurrected from the on-disk file.
2026-07-15 04:26:20 -07:00
Teknium
569b912d7d
feat(agent): explain long provider waits on the live status line (#64775)
Community reports of GPT 'infinitely thinking' are usually a slow or
overloaded provider plus silent retry machinery: the CLI/TUI/Desktop
spinner shows a generic 'cogitating...' verb for the whole wait and the
gateway heartbeat says only 'Working — N min'.

Add AIAgent._emit_wait_notice(): rewrites the live spinner/status line
(thinking_callback → CLI prompt_toolkit widget, thinking.delta → TUI +
Desktop) and updates the activity tracker (included in the gateway's
' Working — N min' heartbeat). Wired at the four wait points:

- non-streaming wait loop: after 30s with no response, the line becomes
  ' waiting on <model> — Ns with no response yet (provider may be slow
  or overloaded; auto-reconnect at Ns)'
- streaming wait loop: same explanation after 30s with no chunks,
  including the long-thinking case
- TTFB / stale-stream kills: '⚠ no response from provider in Ns —
  reconnecting...'
- Codex continuation retries: '↻ model returned reasoning with no final
  answer — asking it to continue (n/3)' instead of silence

Notices are fail-open (display errors never break the wait loop) and
gateway sessions without a display callback still get the improved
activity description.
2026-07-15 00:14:05 -07:00
Teknium
2fc0e3d1aa fix(codex): guard the continuation nudge against role-alternation violations
Follow-up to the salvaged #63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
2026-07-15 00:13:24 -07:00
Ignacio Pastor
05d1ca549b fix(codex): rescue reasoning-only turns that die with 'remained incomplete after 3 continuation attempts'
grok-4.x on the xAI /v1/responses surface sometimes ends a turn with only
reasoning items — no message output item, no tool calls — and those
reasoning items carry no encrypted_content. Two compounding problems:

1. The model occasionally emits its final answer INSIDE the reasoning
   channel, delimited by grok's internal "<response>" tag. The answer
   exists but is classified reasoning-only → finish_reason=incomplete.

2. An interim assistant message holding only plain-text reasoning replays
   as nothing in _chat_messages_to_responses_input, so every continuation
   request is byte-identical to the one that just failed. The model
   deterministically repeats the reasoning-only response until the retry
   budget is exhausted and the turn dies with "Codex response remained
   incomplete after 3 continuation attempts".

Fixes:
- _normalize_codex_response (xai_responses only): salvage the
  <response>-delimited tail from the reasoning text and promote it to
  assistant content; the untagged prefix stays as thinking text.
- Codex-incomplete continuation path: when the interim message has
  nothing the input converter will replay (no content, no encrypted
  reasoning items, no message items), append a user-role nudge so the
  retry actually differs and explicitly asks for the final answer /
  pending tool call. Mirrors the existing _get_continuation_prompt
  pattern used for length truncation.

Observed live with grok-4.20 on xai-oauth (2026-07-13); sibling of the
grok-composer web_search incomplete-loop fix in transports/codex.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:13:24 -07:00
Teknium
07443ea21a test(codex): pin codex_backend issuer in summary-only reasoning sibling test
The #64434 change makes unrecognized issuers trust
response.status='completed' for reasoning-only turns, so this sibling
test (which exercised the old default-path behavior) now pins the Codex
backend explicitly — the surface where reasoning-only still means
'still thinking'.
2026-07-15 00:12:40 -07:00
Teknium
01b7609252 fix(codex): keep GitHub/Copilot Responses on the reasoning-only continuation path
Follow-up to the salvaged #64449: the status-trusting branch flipped
github_responses to 'stop' alongside unknown relays. Copilot fronts the
same OpenAI model family as codex_backend and shows the same
reasoning-only 'still thinking' degeneration, so it stays on the
continuation path. Only unrecognized (other:*) backends trust
response.status='completed' as terminal.
2026-07-15 00:12:40 -07:00
webtecnica
306a3774b6 fix: finish_reason misclassified as incomplete for codex_responses (#64434) 2026-07-15 00:12:40 -07:00
Teknium
0a940972f4
fix(moa): aggregator resolves reasoning like an acting model when slot is unset (#64756)
The aggregator is MoA's acting model, but the main loop's reasoning
gates key off the virtual moa://local identity and never fire — so with
no per-slot reasoning_effort the aggregator silently ran at the backend
default, ignoring the user's reasoning config entirely (#64187).

New _aggregator_reasoning_config(): slot value > full acting-model
resolution via the shared chokepoint (agent.reasoning_overrides for the
slot's model > global agent.reasoning_effort; YAML False stays
'disabled'). Applied to both aggregator call sites (acting turn +
one-shot /moa synthesis).

Reference advisors intentionally keep slot-or-default: inheriting a
global xhigh into every advisor fan-out would silently multiply cost.

Fixes #64187.
2026-07-15 00:11:04 -07:00
Teknium
b34e565957 feat(models): catalog-labeled silent default — GLM-5.2 marked "default": true in the model catalog
The remote model catalog (website/static/api/model-catalog.json) now labels
exactly one entry per provider block with "default": true — z-ai/glm-5.2 for
both OpenRouter and Nous Portal. That labeled entry is the model Hermes
silently lands on when the user never picked one, and it can be rotated by
editing the manifest alone: no release needed.

- model_catalog.py: get_default_model_from_cache() reads the label from the
  in-process/disk cache only — never triggers a network fetch, so hot
  resolution paths (agent build, gateway session setup) stay network-free.
- models.py: get_preferred_silent_default_model() resolves catalog label
  first, PREFERRED_SILENT_DEFAULT_MODEL constant second (offline/fresh
  install). _PROVIDER_SILENT_DEFAULT_OVERRIDES dict replaced by
  _SILENT_DEFAULT_PROVIDERS routing through the shared resolver.
  fetch_openrouter_models() preserves the "default" badge through live
  /v1/models refreshes so the picker shows it.
- scripts/build_model_catalog.py: generator emits the default label so
  regeneration can't drop it.
- website/docs/reference/model-catalog.md: schema documents the new field.
- Salvaged from PR #61141 (@HumphreySun98): bare-provider /model switches
  (/model nous) route through the cost-safe default instead of curated
  entry [0].
- tests: catalog-label precedence, constant fallback, stale-label fallback,
  cache-only (no network) guarantee, and a shipped-manifest contract test
  pinning the labeled entry to PREFERRED_SILENT_DEFAULT_MODEL.

E2E (temp HERMES_HOME): fresh-install constant fallback, shipped-manifest
label read, release-free rotation (relabeled cache -> new default across
models.py, tui_gateway, and gateway empty-model paths) all verified.
2026-07-15 00:10:31 -07:00
HumphreySun98
97375e0f06 fix(models): route bare-provider /model switch through the cost-safe default
`detect_static_provider_for_model` handles a bare provider name typed as a
model (e.g. `/model nous`) by returning `_PROVIDER_MODELS[provider][0]` — the
first curated entry. For metered aggregators whose curated list is ordered
most-capable-first (Nous Portal), entry [0] is the priciest flagship, so
`/model nous` silently switched to it.

This is exactly the billing footgun `_PROVIDER_SILENT_DEFAULT_OVERRIDES` /
`get_default_model_for_provider` exist to prevent (per their docstring, a
missing model "escalated to Opus and billed 863 requests before the user
noticed"). The non-interactive fallback already routes through that cost-safe
helper; the interactive `/model <provider>` path did not.

Route this path through `get_default_model_for_provider` too. Providers
without a silent-default override are unchanged (the helper returns
`models[0]`), so only overridden providers (currently `nous`) change — from
the flagship to the low-cost default.

Adds regression tests: `/model nous` resolves to the cost-safe default, and
non-overridden providers still resolve to their first catalog model.
2026-07-15 00:10:31 -07:00
LeonSGP43
a7784f11fb fix(codex): keep large-request ttfb watchdog active 2026-07-15 00:10:26 -07:00
Teknium
d6c14a952f chore(release): map liuwei666888 in AUTHOR_MAP 2026-07-15 00:10:22 -07:00
Teknium
3804df5b36 fix: harden remaining display-layer surfaces against non-string tool args
Consolidation follow-up on top of @liuwei666888's compressor fix (#52431)
and @Frowtek's ACP render guard (#62588) — completes the class fix across
every display-layer consumer of tool-call arguments:

- agent/context_compressor.py: wrap _summarize_tool_result in a
  never-raises backstop (same pattern as get_cute_tool_message and the
  ACP guard). The per-branch _str_arg coercions keep summaries
  informative; the wrapper guarantees compression can never crash-loop
  on a summary branch we didn't anticipate. Also reject non-dict parsed
  args (a JSON list/scalar would crash every args.get call).
- agent/display.py: build_tool_preview's process branch sliced
  session_id/data without coercion — crashed build_tool_preview and
  build_tool_label on the live tool-progress callback (probed live:
  TypeError 'int' object is not subscriptable). Coerce like the
  sibling branches.
- tests: backstop fuzz matrix (16 tools x 6 hostile value shapes),
  fallback-shape contracts, display process-preview regressions,
  non-dict args guard.
2026-07-15 00:10:22 -07:00
Frowtek
05473428cb fix(acp): don't let a malformed tool argument abort the tool-call render
build_tool_start renders every ACP (Zed) tool call — on the live tool-progress
callback (acp_adapter/events.py) and during session history replay
(acp_adapter/server.py). It called build_tool_title and extract_locations
directly, so a model that emits a malformed argument crashed the render:

- terminal `command` as null/number -> TypeError (len() in build_tool_title)
- delegate_task `goal` as a number  -> TypeError (len())
- read_file `path` as a non-string  -> pydantic ValidationError building a
  ToolCallLocation

A live crash breaks the tool-call event; a persisted one breaks history replay
on every resume of that session. The sibling CLI label builder
get_cute_tool_message was already wrapped for exactly this reason
(agent/display.py: "display must never abort a turn").

Wrap build_tool_start the same way: on any builder failure, fall back to a
minimal, valid start event (tool name as title, resolved kind). The happy path
is unchanged.

Adds tests for the non-string command, path, and goal cases.
2026-07-15 00:10:22 -07:00
liuwei666888
be100d4dae test: add type safety tests for _summarize_tool_result
27 tests covering:
- Non-string args (bool, int, None, list, dict) for all 5 vulnerable sites
- Normal string args (regression tests)
- Edge cases (empty args, invalid JSON, null args, unknown tool)

Verifies the fix prevents TypeError/AttributeError crashes.
2026-07-15 00:10:22 -07:00
liuwei666888
dd923619c7 fix: prevent TypeError in _summarize_tool_result when tool args contain non-string values
When LLMs return non-string parameter values (e.g. bool, int) in tool call
arguments, _summarize_tool_result() crashes with TypeError because it calls
len(), .count(), or slicing directly on args.get() return values.

This causes an infinite crash loop in the TUI — context compression
triggers on session resume, which crashes, which restarts, which triggers
compression again.

Add _str_arg() helper that coerces any value to str, and use it in all 5
vulnerable call sites:
- terminal: len(cmd)
- write_file: content.count()
- delegate_task: len(goal)
- execute_code: len(code)
- vision_analyze: question[:50]
2026-07-15 00:10:22 -07:00
brooklyn!
3f0b0e20e8
Merge pull request #64799 from NousResearch/bb/fix-assistant-ui-tap-compat
fix(tests): make @assistant-ui tap invariant workspace-nesting aware
2026-07-15 02:38:24 -04:00
Teknium
f5c2ea49a4
test: deflake async-delegation interrupt test under CI load (#64767)
* test: deflake async-delegation interrupt test under CI load

test_interrupt_all_signals_running_children failed twice on a loaded CI
worker: the blocker's ev.wait(timeout=5) expired before interrupt_all()
ran, the record finalized on its own, and interrupt_all() found nothing
running (n == 0, interrupted count 0 — the exact CI assertion failure,
reproduced locally by inserting a 5.6s dispatch->interrupt gap).

Raise the internal safety timeouts from 5s to 60s across the file's
gated runners — they exist only as runaway guards (every test releases
its gate explicitly); the pytest-level timeout is the real backstop.
Same flake class as the removed test_crashed_runner_produces_error_
completion (#64431).

* test: fix cross-test completion-event leak (second flake mechanism)

The first CI failure was the 5s guard-timeout race; the rerun exposed a
SECOND mechanism with a different signature: 'completed' == 'interrupted'.
Prior tests (e.g. test_dispatch_rejected_at_capacity) release their gate
and return immediately, but their workers finalize asynchronously — on a
loaded runner the teardown drain races the in-flight _finalize, and the
straggler 'completed' events leak into the NEXT test's queue, where
_drain_one() picks one up instead of the interrupt event. Reproduced
locally: gate release + immediate drain + 0.15s finalize delay leaked 2
events.

Fixes:
- teardown waits (bounded 2s) for active workers to finalize before
  draining, so events land in the owning test
- the interrupt test matches its OWN delegation_id via _drain_for()
  instead of taking whatever event arrives first
2026-07-14 23:36:37 -07:00
Brooklyn Nicholson
91d8e4117c fix(tests): make @assistant-ui tap invariant workspace-nesting aware
The 0.14 upgrade (#63970) dropped the @assistant-ui/store override, so the
whole cluster de-hoisted from root node_modules into apps/desktop/node_modules
under a single shared tap@0.9.3. The test only looked at the root hoist path
(node_modules/@assistant-ui/tap), which no longer exists, and failed on main.

Resolve tap wherever npm places it (root or workspace-nested), and assert a
single shared version across all install sites — strengthening the invariant to
also catch a split tap install, not just split declared ranges.
2026-07-15 02:34:15 -04:00