Switch provider and model together after setup-time auth failure. Serialize global auth-store merges under target-specific locks and preserve auth-to-shared lock ordering for profile OAuth refreshes.
Widens the deepseek-v4-flash addition to the whole stale-snapshot class:
- deepseek-v4-pro: $1.74/$3.48 → $0.435/$0.87, cache-read $0.003625
(DeepSeek's 2026-07 price cut; every pro session was over-reporting 4x)
- deepseek-chat / deepseek-reasoner: deprecated 2026-07-24, now alias
v4-flash non-thinking/thinking modes — repriced to match flash
(reasoner was $0.55/$2.19 with no cache rate)
- cache_read added to every row; pricing_version unified at
deepseek-pricing-2026-07
- invariant tests: aliases price identically to flash; every deepseek
row carries cache_read < input
DeepSeek's /models endpoint returns no pricing, so direct-provider routes fall back to the _OFFICIAL_DOCS_PRICING snapshot. The table included deepseek-v4-pro but not the newer deepseek-v4-flash, so flash sessions reported $0.00 with cost_source "none". Add the flash entry (values from DeepSeek's official pricing page, mirroring the v4-pro entry; DeepSeek bills no separate cache-write cost) plus two regression tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two turns interleaving on one session corrupt the durable transcript:
flushes race (user rows persist out of arrival order), the identity-marker
dedup over shared history dicts can swallow a row, and the second turn
runs on a history base that never saw the first turn's exchange. The
dispatch route that lets the second turn through the busy guard is not
yet identified.
Add note_turn_start (build_turn_context) / note_turn_persisted
(_persist_session funnel): one WARNING naming both turn_ids when a turn
starts before the previous turn's turn-end persist. Ownership transfer
keeps a crashed turn from warning more than once; the unconditional clear
makes the tripwire under-report rather than double-report under a real
overlap. Log-only, no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up on the salvaged #64611 commit: the original guard blocked the
install tree unconditionally, which would have broken the legitimate
'developing Hermes from a source clone' CLI flow (launching hermes inside
the repo and getting its AGENTS.md as project context).
Refined policy:
- resolve_context_cwd(): validates configured paths (missing dir -> None +
warning) but honors an EXPLICIT install-tree cwd verbatim — deliberate
user choice.
- build_context_files_prompt(): blocks only the cwd=None -> os.getcwd()
FALLBACK into the install tree, with a new allow_install_tree_fallback
param. system_prompt.py passes it for platform cli/tui (launch dir is
the user's real shell cwd there); desktop/gateway surfaces keep the
guard (their fallback dir is self-spawned, never user-picked).
- Warning log names the resolved dir and the terminal.cwd remedy.
E2E-verified all five scenarios: desktop fallback blocked, in-tree CLI dev
keeps AGENTS.md, explicit install-tree cwd honored, invalid TERMINAL_CWD
falls to None then blocked, normal workspace loads.
- Refresh _OFFICIAL_DOCS_PRICING fireworks entries against current
docs.fireworks.ai/serverless/pricing: qwen3p6-plus is gone (replaced
by qwen3p7-plus); add glm-5p2/5p1, kimi-k2p7-code, deepseek-v4-flash,
minimax-m3/m2p7, gpt-oss-120b/20b, and the routers/*-fast tiers with
their distinct higher rates.
- Picker pricing via get_pricing_for_provider('fireworks'): pure dict
transform over the shared models.dev in-memory/disk cache (1h TTL) +
_pricing_cache memoization — no new network call on the picker path.
- Wire pricing display into the generic api-key-provider setup flow so
Fireworks model pickers show $/M columns like OpenRouter/Nous do.
- Invariant tests: plugin fallback_models all priced, fast tiers price
higher than standard, every row carries cache_read < input.
Fireworks-hosted sessions previously showed estimated_cost_usd = 0
because (a) _OFFICIAL_DOCS_PRICING had no Fireworks entries and (b)
resolve_billing_route() had no branch for provider="fireworks",
falling through to billing_mode="unknown".
Adds entries for the three Fireworks models hermes operators are
most likely to route through (Kimi K2.6, DeepSeek V4 Pro, Qwen3.6-Plus)
and a routing branch that triggers on either explicit
provider="fireworks" or api.fireworks.ai base_url match. Mirrors the
recently-merged MiniMax addition pattern; pricing snapshot sourced
from https://docs.fireworks.ai/serverless/pricing and the per-model
pages on fireworks.ai.
Tests cover: (a) full Fireworks model id resolves to the snapshot
entry, (b) base_url alone is sufficient to route, (c) end-to-end
estimate returns "estimated" status with the expected dollar amount.
A follow-up upstream issue is open proposing a dynamic pricing
source (e.g. litellm's pricing JSON) as a permanent fix to the
PR-per-model treadmill that this snapshot keeps adding to.
* feat(analytics): record auxiliary model usage per task in session accounting
Auxiliary LLM calls (vision, compression, title_generation, web_extract,
session_search, ...) discarded their token usage, leaving dashboard
analytics blind to aux model spend (issue #23270).
- hermes_state.py: session_model_usage gains a task PK dimension
(''=main loop) via v22 table-rebuild migration (SQLite can't alter a
PK); record_auxiliary_usage() writes per-(model,provider,task) deltas
WITHOUT touching sessions counters (gateway overwrites those with
absolute main-loop totals — folding aux in would double-count or be
clobbered). Aux rows never inherit the session's main-loop route.
- agent/aux_accounting.py: ContextVar ambient accounting context
(mirrors the portal_tags conversation context); record_aux_usage()
normalizes usage via usage_pricing.normalize_usage, estimates cost,
and is strictly best-effort. moa_reference/moa_aggregator excluded —
conversation_loop already folds MoA usage+cost into the main delta.
- agent/auxiliary_client.py: _validate_llm_response is the recording
chokepoint — every successful non-streaming aux response passes
through it exactly once, sync and async, including fallback paths
(model read from the response itself stays accurate across
fallbacks).
- run_agent.py: run_conversation publishes/resets the accounting
context; agent/title_generator.py republishes on its bare thread.
- hermes_cli/web_server.py: /api/analytics/usage folds aux rows into
by_model (aux-only models finally appear) and adds a by_task
summary; /api/analytics/models surfaces aux rows on the Models page.
Design per review of PR #62850 by @eeksock (thread-local + separate
auxiliary_usage table): rebuilt on ContextVar (async-safe — thread-local
cross-attributes concurrent coroutines on one event loop) and the
existing session_model_usage table instead of a parallel accounting
path, extended beyond vision to every aux task, and wired the analytics
endpoints so the dashboard actually shows it. Credit to @eeksock for
the approach and @tboatman for the detailed root-cause analysis.
* test(moa): match _validate_llm_response mock to new accounting-hint signature
* test(aux): accept accounting-hint kwargs in remaining _validate_llm_response mocks
Extends the conversation=<id> Portal tag (salvaged from PR #65183 by
@J-SUPHA) from main-loop-only to every LLM call in a conversation:
- agent/portal_tags.py: ContextVar-based conversation context.
nous_portal_tags() falls back to the ambient id when no explicit
session_id is passed, so every aux tag site (auxiliary_client,
chat_completion_helpers summary path, web_tools) inherits the tag
with zero per-call-site plumbing. Ambient id wins over explicit
per-segment ids since it carries the lineage root.
- hermes_state.py: SessionDB.get_conversation_root() — public wrapper
over the lineage walk; returns the ROOT session id, so one
user-facing conversation keeps a single conversation= value across
context-compression rotation, and delegate subagent trees tag as
their parent conversation.
- run_agent.py: run_conversation() publishes the root id for the turn
and resets it in finally. _conversation_root_id() resolves via
_parent_session_id for subagents.
- agent/moa_loop.py: MoA reference fan-out workers now run under
propagate_context_to_thread so advisor slots attribute to the acting
conversation (also fixes approval-callback propagation on that path).
- agent/title_generator.py: bare title thread republishes the context
from its session id (spawned after turn reset).
Tests: ContextVar semantics, cross-context isolation, thread-hop
propagation, lineage-root resolution incl. cycle guard.
The per-session record store is now the ONLY cwd mechanism. Deleted:
- env.cwd_owner stamping + prev_owner threading (terminal_tool): the
shared env no longer carries ownership metadata at all
- _resolve_command_cwd's env/prev_owner params: resolution is
workdir > session record > config/override default
- file_tools._live_cwd_if_owned + _get_live_tracking_cwd: path
resolution never consults the shared env's live cwd
- file_tools._last_known_cwd + _remember_last_known_cwd +
_last_known_cwd_for: the #26211 preserved-anchor registry is
subsumed by the session record, which never lived on the env and
therefore cannot be lost to env cleanup. The _get_file_ops
stale-cache rescue now writes the record instead.
- env recreation (both _get_file_ops and terminal_tool) seeds the
fresh env from override > session record > config
Why no transition fallback: the legacy state was process-local and
in-memory exactly like the record store — after a restart both start
empty, and within a running process every legacy write site has been
dual-writing the record since step 1. There is no populated-legacy/
empty-record state to fall back for.
Tests updated to drive the record store instead of the deleted
mechanism; the cross-session isolation suite now asserts the same
behavior contracts (no leak, cd isolation, #26211 persistence)
against the new architecture, plus a new "session C inherits nothing"
case that the old ownership guard could not express.
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 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
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.
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.
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.
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.
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
_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.
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).
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>
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'.
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.
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.
Fixes#64025
The hermes-tools MCP server was fetching each tool's JSON schema
into params_schema but never passing it to FastMCP's add_tool(),
so all published tools had an empty **kwargs signature. MCP clients
couldn't see parameters and arguments were dropped at dispatch.
This fix:
- Adds _signature_from_schema() to convert JSON schemas to Python
function signatures with type annotations
- Attaches the generated signature/annotations to each handler closure
so FastMCP introspects the real parameter structure
- Filters out None values before dispatch to avoid forwarding unset
optional parameters
Impact: web_search, browser automation, vision, and other Hermes tools
are now properly callable from the codex_app_server runtime.
The just-merged auxiliary.<task>.reasoning_effort shorthand applied
ensemble-wide to MoA (one value for every advisor) — wrong granularity.
Per-slot preset config supersedes it:
moa:
presets:
deep_review:
reference_models:
- {provider: ..., model: ..., reasoning_effort: low}
- {provider: ..., model: ..., reasoning_effort: xhigh}
aggregator:
{provider: ..., model: ..., reasoning_effort: high}
- Remove reasoning_effort from the moa_reference/moa_aggregator
DEFAULT_CONFIG blocks; _get_task_extra_body now warns-and-ignores the
key on MoA tasks, pointing at the preset config
- Guard tests: MoA aux blocks must not regrow the key; task-level value
is rejected with the pointer warning
- Docs: configuration.md notes the MoA exception and links the MoA page
Every auxiliary task block (vision, web_extract, compression,
title_generation, curator, background_review, moa_reference, ...) now
accepts a reasoning_effort shorthand:
auxiliary:
compression:
reasoning_effort: low
vision:
reasoning_effort: none
_get_task_extra_body() folds it into extra_body.reasoning, which every
auxiliary wire already translates: chat.completions passes it through,
the Codex Responses adapter maps it to top-level reasoning/include, and
the Anthropic auxiliary adapter now forwards it into
build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None).
An explicit extra_body.reasoning on the same task wins over the
shorthand. Invalid levels are ignored with a warning. Empty string
(the shipped default) is a no-op — zero behavior change.
Config: reasoning_effort added to all 16 auxiliary task blocks in
DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
The platform hint in PLATFORM_HINTS['telegram'] always encouraged rich
Markdown constructs (tables, task lists, math, collapsible details) even
when rich_messages: false (the default). This caused the agent to produce
formatting that MarkdownV2 cannot render, especially broken on Telegram Web.
Split the hint into a base hint (MarkdownV2-compatible) and a
TELEGRAM_RICH_MESSAGES_HINT extension. The extension is conditionally
appended in system_prompt.py only when
platforms.telegram.extra.rich_messages is true.
Fixes#57122
Return actionable errors when HERMES_WRITE_SAFE_ROOT blocks a path instead of
labeling every denial as a protected credential file. Wire the helper through
write_file, patch, delete/move, and the Copilot ACP shim; sync docs examples.
- Nudge text now warns that repeated protocol violations will block the
task and require manual intervention, so the model understands the
consequence of ignoring the nudge.
- Kanban docs restructured to clearly separate the two defense layers:
agent-side prevention (nudge, from #64350) and dispatcher-side
recovery (bounded retry, from this PR).
- Two new integration tests verifying the nudge mentions blocking and
that the agent-side and dispatcher-side budgets are independent.
scan_skill_commands() had two collision bugs in the same loop body:
1. Core-command collision: a skill whose normalized slug matches a core
Hermes command name or alias (e.g. "skills", "learn", "bg") would
get an auto-generated /command that shadows the core command in the
gateway dispatch path (skill map is consulted before built-in
handlers). The skill command silently overrode the core command.
2. Inter-skill slug collision: the seen_names set deduped on the raw
frontmatter name, but the command map was keyed by the normalized
slug. Two distinct names collapsing to the same slug (e.g.
"git_helper" vs "git-helper") both passed the dedup, and the second
silently clobbered the first.
Fix: add two guards in scan_skill_commands() after slug normalization:
- resolve_command(cmd_name) check skips skills colliding with any core
CommandDef (name or alias), logging a warning. Uses the existing
resolve_command() API so aliases and case variants are covered
without a separate cache. The skill remains loadable via /skill.
- cmd_key in _skill_commands check dedups on the resolved slug,
first-wins (preserving local-before-external precedence), logging
a warning naming the shadowed skill.
Combines and supersedes #31204 (@cyrkstudios), #53450 (@Gridzilla),
#50304 (@petrichor-op), and #63305 (@Vissirexa).
Co-authored-by: cyrkstudios <cyrkstudios@users.noreply.github.com>
Co-authored-by: Gridzilla <Gridzilla@users.noreply.github.com>
Co-authored-by: petrichor-op <petrichor-op@users.noreply.github.com>
Co-authored-by: Vissirexa <Vissirexa@users.noreply.github.com>
Add a bounded turn-end stop guard for kanban workers. When a worker
tries to exit with finish_reason=stop without having called
kanban_complete or kanban_block, inject up to two synthetic nudges
so the conversation loop continues instead of exiting cleanly (which
the dispatcher records as protocol_violation).
Mirrors the existing verify-on-stop pattern: same ephemeral scaffolding
flag (_kanban_stop_synthetic), same role-alternation contract, same
_pending_verification_response fallback for budget exhaustion.
Disabled by default (gated on HERMES_KANBAN_TASK env var set by the
dispatcher); kill switch via HERMES_KANBAN_STOP_NUDGE=0.
Salvaged from #62262 by @mdc2122. The original branch was 272 commits
behind main with ~538 files of stale-base reversions; this salvage
applies only the 4 substantive files (agent/kanban_stop.py,
conversation_loop.py insertion, run_agent.py _EPHEMERAL_SCAFFOLDING_FLAGS,
tests/agent/test_kanban_stop.py).
OpenAI lets ChatGPT-plan Codex users bank rate-limit reset credits, but
until now they could only be redeemed from the Codex CLI/app or the
website. This wires the same backend API into Hermes:
- /usage on the openai-codex provider now shows "You have N resets
banked - use /usage reset to activate" (parsed from the
rate_limit_reset_credits field the /usage endpoint already returns).
- New /usage reset subcommand (CLI + gateway) redeems one banked
credit via POST .../rate-limit-reset-credits/consume with a UUID
idempotency key, mirroring codex-rs backend-client semantics
(PathStyle /wham vs /api/codex, ChatGPT-Account-Id header,
reset/nothing_to_reset/no_credit/already_redeemed outcomes).
- Guard: redemption is refused while no rate-limit window is fully
exhausted, since a banked reset restores the FULL 5h + weekly
allowance and spending it early wastes it. /usage reset --force
overrides. Zero banked credits and non-codex providers are refused
with clear messages; nothing_to_reset reports the credit was NOT
spent.
- i18n: new gateway.usage.unknown_subcommand / reset_wrong_provider
keys across all 16 locales; docs updated (cli.md, messaging index).
Tested with unit tests plus a real-socket E2E against a local fake
Codex backend exercising redeem/guard/force and the /usage hint.
Include the Hermes client name and version with Gemini inference, model and tier checks, and TTS requests. Add focused coverage for the request headers and keep the Gemini-specific context scoped to Google Gemini endpoints.