Follow-up on kshitijk4poor's cherry-picked references:
- pitfalls.md rewritten from the raw-socket blender_exec() frame to the
MCP-tool frame (dropped 'MCP server is optional, talk to the socket
directly' — now the anti-pattern; dropped TCP-helper internals items;
kept all bpy/addon knowledge: empty code results in 5.x, temp-file
readback, ops-vs-data context, engine names by version, GPU setup)
- recipes.md: blender_exec -> execute_blender_code in the agent-side
verification snippet
- SKILL.md: reference-file table added to Quick Reference, version
2.1.0, kshitijk4poor added to authors
- docs page regenerated (scoped to this skill)
Fixes#62452. Two amplifiers turned one slow auxiliary route into a
per-turn multi-minute stall:
1. Fallback candidates inherited the exact effective_timeout the primary
was called with. When the primary's deadline was short (tuned or
already burned), an independently healthy fallback died on the same
clock — the reporter's 163k-token compression needed ~90s on the
fallback and got the primary's 30s, every turn. fallback_chain
entries may now declare their own 'timeout' (seconds); both fallback
candidate call sites (sync + async) resolve it via
_fallback_entry_timeout, label-scoped so only configured-chain
candidates are affected. No entry timeout → task-level timeout,
preserving existing behavior.
2. A session whose transcript structurally cannot be summarized within
the deadline re-attempted every 60s, re-burning the full timeout on
every subsequent turn. Consecutive timeout-class failures now
escalate the cooldown 60s → 300s → 900s (capped); any successful
summary or session reset clears the streak. Timeout classification
takes precedence over the streaming-closed 30s rung ('timed out'
also matches _is_connection_error) and now recognizes the SDK's
'Request timed out.' phrasing.
Fail-safe behavior is unchanged: all messages are preserved when every
candidate fails; the cooldown only spaces out retries.
The served-profiles block in _start_secondary_profile_adapters references
PairingStore, but the class's only import in gateway/run.py is method-local
inside __init__ — so the reference raised NameError at runtime, silently
swallowed by the enclosing try/except ('could not record served_profiles').
Result: multiplexing gateways never created per-profile pairing stores, and
authz pairing checks for secondary profiles fell through to the global
whitelist. Also masked the served_profiles runtime-status write.
One-line fix (local import alongside write_runtime_status) + regression
tests that drive the real method and assert the stores materialize,
verified red without the import and green with it.
Surfaced during the profile-routing sweep by @CocaKova's PR #61689, which
included the same fix as part of a larger feature.
* fix(computer-use): target Linux app windows reliably
Resolve app filters through the canonical cua-driver MCP app metadata and join running app PIDs back to windows. Preserve an exact selected window across capture_after, support direct capture by pid/window_id, and send the active window ID for coordinate pointer actions on Linux.
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
* fix(computer-use): address review on PR #63725
Address three review comments from @f-trycua:
1. type_text, press_key, and hotkey now carry _active_window_id and
fail closed when it is missing. Previously they sent only the PID,
so CUA Driver fell back to the first window for that PID — input
could reach the wrong window in multi-window apps.
2. Coordinate scroll x/y are now capability-gated behind
input.scroll.coordinates. CUA Driver 0.7.1 Linux schema rejects
x/y on scroll; omitting them when the driver doesn't advertise
support avoids the schema rejection while still routing via
window_id.
3. Windows are sorted by z_index descending (higher = front, per CUA
Driver semantics) instead of ascending. Null z_index (Wayland) is
coerced to 0 in _ingest_windows so it doesn't crash the sort and
sorts to the back instead of being selected as the capture target.
---------
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
The cross-turn stale-call circuit breaker (_check_stale_giveup in
agent/chat_completion_helpers.py) raises a RuntimeError when the
provider has been unresponsive for N consecutive stale attempts.
This error was classified as FailoverReason.unknown (retryable=True,
should_fallback=False), causing the retry loop to burn all max_retries
against the same dead provider — each retry hitting the circuit breaker
instantly with zero network overhead — before fallback was attempted.
Add a classification rule (section 7b in classify_api_error) that
recognizes the stale-breaker RuntimeError by its signature phrases and
classifies it as FailoverReason.timeout with retryable=False,
should_fallback=True. This makes the retry loop skip retries and go
straight to fallback provider activation on the first hit.
Test: test_stale_breaker_runtime_error_triggers_fallback_not_retry
The #54878 self-heal (SessionStore.get_or_create_session) drops a routing
key pointing at a session already ended in state.db and recovers/recreates
a fresh session_id under the same session_key. The #54947 fix (agent-cache
cache-hit guard in gateway/run.py) treats a cached agent whose snapshot
session_id differs from the current session_id, under the same
session_key, as an intentional /resume-/branch-style switch between two
live sibling conversations, and reuses it unchanged to protect the prompt
cache.
These two fixes compose incorrectly: when the #54878 self-heal just fired,
the cached agent's session_id is not a live sibling — it's the dead session
just routed away from. #54947's "different session_id -> reuse freely" rule
reuses it anyway. The stale agent runs the turn, and the post-run "session
split" sync (agent.session_id != session_id) then writes the routing key
straight back onto the dead session_id, undoing the self-heal. This repeats
on every subsequent message until an interrupt (e.g. /stop) happens to race
in before that post-run sync, silently discarding conversation context.
Reproduced live on the engineering gateway (2026-07-12, routing key
agent:main:telegram:dm:170829464:544520): 5 consecutive self-heal log lines
over ~40 minutes, each followed by the dead session_id being reused and
re-synced back, until an interrupted /stop finally let a fresh session
stick — at which point all prior context was gone.
No open upstream issue tracks this specific interaction as of 2026-07-12
(checked #54878, #54947, #59580, #59597, #61220 — all cover adjacent but
distinct edges of the self-heal / agent-cache system).
Fix: before applying #54947's reuse-on-mismatch rule, check (outside the
cache lock, via SessionStore._is_session_ended_in_db) whether the cached
snapshot's session_id is itself ended in state.db. If so, treat it as a
stale self-heal artifact and evict/rebuild fresh -- same as a genuine
cross-process write -- instead of reusing it. Re-validates the peeked
verdict against the tuple actually held under the lock so a race can't
apply a stale verdict to a different (possibly live) cache entry.
Tests: tests/gateway/test_stale_self_heal_agent_cache_eviction.py (5 new
cases: dead-session eviction, live-sibling reuse preserved [#54947 intact],
cross-process invalidation preserved [#45966 intact], same-session_id dead
edge case, lock-race re-validation). Full tests/gateway/ suite: 14 failed,
9040 passed, 11 skipped -- all 14 failures verified pre-existing on
unpatched main (confirmed via git stash + re-run), unrelated to this
change.
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.
Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
GatewayRunner._prepare_inbound_message_text() interpolated
source.user_name — the platform-supplied, user-settable display name —
directly into the message text of every turn in a shared multi-user
session: f"[{source.user_name}] {message_text}". Shared sessions are the
default for any threaded conversation (thread_sessions_per_user defaults
to False) and apply to any group with group_sessions_per_user=False, so
no special configuration is needed to reach this path.
An unescaped display name containing embedded newlines could therefore
masquerade as a new markdown section (a fake "## Override" heading)
inside the live conversation the model reads on every turn — the same
indirect-prompt-injection vector gateway/session.py's
build_session_context_prompt() already guards against for the identical
user_name field via _format_untrusted_prompt_value(), which was never
applied to this sibling call site.
Add neutralize_untrusted_inline_text() alongside the existing helper in
gateway/session.py: it collapses embedded newlines/control characters to
a single inert line without JSON-quoting, so inline "[Name] message"
formatting is preserved byte-for-byte for the common case (unlike
reusing _format_untrusted_prompt_value directly, which would add visible
quote marks to every sender prefix). Wire it into the sender-prefix
construction in gateway/run.py.
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.
The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
Bug 2 of #28156: the picker offered us./global. inference profiles to
EU-region endpoints (unroutable — AWS rejects them regardless of
credentials) and _RECOMMENDED hardcoded us.anthropic.* ids, so non-US
pickers pinned profiles their endpoint can't invoke.
- bedrock_model_routable_from_region(): geo-prefixed profiles are only
offered in their own geography (full AWS prefix set incl. apac./jp./
ca./sa./me./af.); bare ids and global.* pass everywhere; unknown
region shapes hide nothing.
- Recommendations match geo-agnostically on the base model id, so an EU
picker pins eu.anthropic.claude-sonnet-4-6; in-region geo profiles
sort above global.* for the same model (addresses the global.*-first
ordering complaint from the issue thread).
- Dedup generalized from (us., global.) to all profile prefixes.
Rewrites the cherry-picked test to import os locally and monkeypatch the
resolver seams instead of patching bedrock_adapter internals; adds the
inverse assertion (no bearer -> AnthropicBedrock SDK path preserved).
Three fixes for the Bedrock Claude path:
1. Streaming fallback: When AnthropicBedrock SDK raises 'Unexpected event
order' (SDK misparses Bedrock error events as message_start), auto-switch
to native Converse API for the rest of the session instead of failing
after 3 retries.
2. Image base64 decode (#33317): data URL payloads were passed as base64
strings to source.bytes, but boto3 re-encodes at the wire layer. Now
decoded to raw bytes before passing to Converse API.
3. Bearer token routing (#28156): Users with AWS_BEARER_TOKEN_BEDROCK are
now routed through Converse API regardless of model, since the
AnthropicBedrock SDK only supports SigV4 signing.
3 new tests. 121 bedrock_adapter tests passing.
The docs search theme (@easyops-cn/docusaurus-search-local) defaults
fuzzyMatchingDistance to 1, so every term also matched words one edit
away. Two user-visible failures on the 14.4 MB production index:
- Wrong results: 'keet' returned 'Microsoft Teams Meetings',
'google_meet', 'Keep the Model Loaded' etc. — 'meet' and 'keep' are
both one edit from 'keet', and the stemmer indexes 'meetings' as
'meet'.
- Search appearing to die: fuzzy matching multiplies the generated
lunr queries (distance matrix x maybe-typing variants x
leave-one-out terms — up to 210 queries per keystroke on multi-word
input), and fuzzy REQUIRED terms are the expensive scan kind. A
typo'd 3-word query stalled the single-threaded search Web Worker
for 25+ seconds; every later keystroke's search queued behind it,
so the bar stopped returning results.
Setting fuzzyMatchingDistance: 0 keeps exact-word-or-prefix semantics
(keet -> keet*), which is the behavior users asked for. Validated by
running the plugin's shipped smartQueries/tokenize code against the
downloaded production search-index.json: legitimate queries (cron,
telegram, prefix 'memor') return identical results; worst-case typo
queries drop from 210 queries / 365-532ms per keystroke to 50-105 /
53-188ms; false 'keet' matches gone.
Follow-up hardening on top of #64158 (@DavidMetcalfe):
Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
counterpart to the deliberately tolerant normalize_moa_config(). Flags
half-filled slots, empty reference lists, recursive moa slots, naming the
exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
and returns 422 with the specific problems instead of silently swapping the
user's preset for hardcoded defaults (#64156). Also declares
fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
client round-trips no longer erase hand-set values.
Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
autosave is deferred (not repaired) while any slot is half-filled, and
flushes once the model pick completes the edit. Mid-edit UI state is never
repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
model shows the 'Model' placeholder instead of vanishing.
Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).
Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.
Fixes#64156
Three fixes in model-settings.tsx:
1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips
reference slots with empty model before any autosave (the 600ms debounce
was sending half-filled provider-but-no-model slots to the backend, where
_clean_slot rejects them and _normalize_preset falls back to hardcoded
defaults). Presets with zero valid refs keep their empty reference_models
array rather than being silently dropped. The aggregator slot is also
sanitized when its model is empty.
2. Add withActive() to MoA provider dropdowns: the reference and aggregator
provider Selects filtered to authenticated-only, so unauthenticated
current values (e.g. openai-codex) rendered blank. Mirror the existing
pattern from the model Select.
3. Add generation counter to scheduleMoaSave(): stale save responses could
overwrite newer state. Bump a counter on each save and skip setMoa/setError
if a newer save was scheduled in the meantime.
Follow-ups on the salvaged #20096 profile-routing feature:
- _profile_name_for_source now returns None unless gateway.multiplex_profiles
is on. Routing stamps source.profile, which namespaces session/batch keys,
but the profile-scoped agent run only activates under multiplexing — without
the gate, configured routes with multiplexing off split batch/session keys
into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
that builds batch keys via build_session_key (telegram, whatsapp, matrix,
feishu, wecom, weixin) — routing is platform-generic, so the batch-key
namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
(ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
docs/profile-routing.md (routing requires multiplexing), fix the
platform-only specificity row (0, not 1), and document profile_routes in
website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
build_source end-to-end stays in agent:main when off).
Completes the review's ask for "adapter-to-session-key integration coverage
for Discord and a non-Discord platform" on #20096.
Drives a concrete adapter's real BasePlatformAdapter.build_source with an
injected gateway_runner, asserts the matched route's profile is stamped on
the source, and that build_session_key scopes the key under agent:<profile>:
(versus the shared agent:main: namespace). Covers Discord and Telegram — the
Telegram case is the bug-#2 path that previously fell through to default.
Adds a regression anchor: without gateway_runner, profile stays None and the
key lands in agent:main (the silent fallback the fix removes for non-Discord).
Co-Authored-By: Claude <noreply@anthropic.com>
Addresses hermes-sweeper review on #20096.
Problem 1 (profile_routing.py): route matching returned True on a chat_id
hit before the guild_id constraint was consulted, so a route declaring both
guild_id and chat_id matched on chat_id alone. Restored conjunctive (AND)
semantics — every declared discriminator must hold; hierarchical parent_chat_id
matching is preserved. Added a regression test for the guild+chat case.
Problem 2 (base.py / run.py): gateway_runner was injected only when an adapter
pre-declared the attribute, and only Discord did — so build_source never called
_profile_name_for_source for Telegram/Feishu/Slack/etc., despite the
platform-generic claim. Declared gateway_runner on BasePlatformAdapter and made
the plugin-registry injection unconditional, so profile routing now reaches
every platform. Added non-Discord (Telegram) resolution coverage and an
injection-inheritance test.
Also adds docs/profile-routing.md documenting gateway.profile_routes
(matching rules, specificity, profile isolation) — requested in review.
Co-Authored-By: Claude <noreply@anthropic.com>
Two follow-ups observed after deploying profile routing:
1. sessions.profile_name was NULL even when the agent ran inside the
routed profile scope. _insert_session_row never wrote it,
get_or_create_session / reset_session never passed it through, and
the agent-side _ensure_db_session fallback had no way to read it.
- Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
auto-adds it on existing DBs.
- _insert_session_row takes profile_name and writes it.
- SessionStore passes source.profile (or old_entry.origin.profile
on reset) into db_create_kwargs.
- _ensure_db_session reads the active profile via
get_active_profile_name() inside _profile_runtime_scope.
2. DiscordAdapter._text_batch_key called build_session_key without
profile=, so the batch key always landed in agent:main even when
the routed profile differed — diverging from the agent session
key namespace (agent:crypto-trader, agent:ai-expert, ...).
Pass event.source.profile through so both namespaces agree.
Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord🧵...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
load_gateway_config only forwarded the top-level multiplex_profiles
key, ignoring the nested gateway.multiplex_profiles form. The latter
is what `hermes config set gateway.multiplex_profiles true` writes,
so users who ran that command got multiplex_profiles=False silently
— no warning, no fallback, profile_routes just stopped matching.
Loader now checks the top-level key first, falls back to the nested
gateway section, and only then defaults to False. Same precedence is
applied to other nested-form keys (profile_routes already did this).
Tests cover: top-level honored, nested honored (regression test for
the silent-fallback bug), default False, top-level overrides nested.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_adapter_credential_fingerprint only looked at adapter.token directly,
but Discord (and similar) adapters store the bot token on their config
sub-object, not on self. Every Discord adapter in a multiplexed
gateway therefore returned None, the same-token conflict check was
silently skipped, and N adapters all polled the same bot token —
producing a per-message race where whichever adapter won the GIL
answered the user.
Adds a config-token fallback (token, then bot_token) so the check
actually fires for config-backed adapters. Direct adapter.token
still takes precedence when both exist.
Tests cover: config-backed token produces a fingerprint, distinct
tokens produce distinct fingerprints, direct token wins over config,
config without token attributes returns None.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
STANDARD_PROFILES, normalize_profile, validate_profile_name, and
is_standard_profile in hermes_constants were superseded by
hermes_cli.profiles.{normalize_profile_name, validate_profile_name}
but never removed. profile_routing.py is updated to import from the
canonical location; the old helpers are deleted.
Lazy import inside parse_profile_routes avoids the circular dependency
at module load time (hermes_constants -> hermes_cli -> hermes_constants).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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).
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.
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).
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.
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
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.
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.
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.
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).
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.
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.
* 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.