Skip to content

feat(reasoning): accept "max" reasoning effort end-to-end with nearest-down clamp - #49644

Closed
arminanton wants to merge 260 commits into
NousResearch:mainfrom
arminanton:feat/reasoning-max-effort
Closed

feat(reasoning): accept "max" reasoning effort end-to-end with nearest-down clamp#49644
arminanton wants to merge 260 commits into
NousResearch:mainfrom
arminanton:feat/reasoning-max-effort

Conversation

@arminanton

Copy link
Copy Markdown
Contributor

Problem

GitHub Copilot / GitHub Models serve some models (e.g. GPT-5.x, Claude opus-4.7/4.8) with a reasoning-effort ceiling above high, but Hermes couldn't reach it:

  1. max was not in the effort vocabulary — setting reasoning_effort: max (or /reasoning max) silently fell back to the default.
  2. The request path force-downgraded xhighhigh unconditionally (a stale guard from the free-tier deployment), and for any other unsupported level it snapped to medium. So a high-effort request on a model whose real ceiling is xhigh collapsed all the way to medium.

Fix

  • hermes_constants.VALID_REASONING_EFFORTS gains max; parse_reasoning_effort documents it as the universal vocabulary the request path maps down per model.
  • run_agent.AIAgent._github_models_reasoning_extra_body — a rank-based nearest-down clamp. Honor the requested level when the live catalog (capabilities.supports.reasoning_effort) lists it; otherwise pick the strongest supported level at or below the request (so maxxhigh for GPT-5.x), falling back to the weakest only when nothing is at/below. Downgrades are logged at DEBUG instead of happening silently.
  • max threaded through every remaining validation gate so it doesn't no-op at any layer: the chat_completions transport gate (which would otherwise reset maxmedium before the request ships), the /reasoning slash handlers + subcommand list (CLI and gateway), the effort-selection ordering, the config-loader docstring, and batch_runner's --reasoning_effort validation.
  • hermes_cli.models.COPILOT_REASONING_EFFORTS_GPT5 gains xhigh so the static fallback (used when the live catalog is unavailable) matches the paid GPT-5.x ceiling.

Tests

tests/agent/test_reasoning_max_effort.py (10 tests): the vocabulary additions and the real clamp (exercised by binding the unbound method to a lightweight stub) — maxxhigh for GPT-5.x, max honored when supported, xhigh no longer force-downgraded, nearest-down selection, weakest-fallback. ruff clean; all touched files compile.

Relationship to #49184 (P1)

Builds on #49184 (route Claude-on-Copilot to /v1/messages). The deeper reasoning-effort levels are a capability of the /v1/messages (Anthropic Messages) path that #49184 establishes — under the legacy /chat/completions route the effort ceiling doesn't apply, so this change is most meaningful once #49184 lands. It is independent of the limit-numbers PR (#49449): different concern (effort vocabulary + clamp vs context/output numbers), and the two can land in either order on top of #49184.

Prior art

Behavioral superset of #34199 — that PR adds the max constant but not the clamp logic that makes max actually resolve to a usable level on GPT-5.x. Fixes #29248.

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard provider/copilot GitHub Copilot (ACP + Chat) P2 Medium — degraded but workaround exists labels Jun 20, 2026
@arminanton
arminanton marked this pull request as ready for review June 20, 2026 14:29
teknium1 and others added 24 commits June 20, 2026 10:45
…rch#49697

Re-indent the salvaged title= lines to spaces (prettier), and map
alelpoan@proton.me in the release author map.
…hing (NousResearch#36733)

* feat(setup): Blank Slate setup mode — minimal agent, opt in to everything

Adds a third first-time setup option alongside Quick Setup and Full Setup.
Blank Slate forces ON only what an agent needs to run — provider & model,
the File Operations toolset, and the Terminal toolset — and turns
everything else OFF, then walks the user through opting each capability
back in.

What it does:
- platform_toolsets.cli = [file, terminal] (explicit, authoritative list)
- agent.disabled_toolsets = every other known toolset (web, browser,
  code_execution, vision, memory, delegation, cronjob, skills, image_gen,
  kanban, …). Applied last in the resolver, so it overrides the
  non-configurable platform-toolset recovery that would otherwise re-add
  toolsets like kanban — guaranteeing a true blank slate.
- Optional config features off: compression, memory + user-profile capture,
  checkpoints, smart model routing, auto session reset.
- Bundled skills default to NONE (reuses the .no-bundled-skills marker);
  offers to seed the full catalog.
- Walks through tools / plugins / MCP / messaging, all opt-in.

Proven end-to-end: with the Blank Slate config, model_tools.get_tool_definitions
emits exactly 6 schemas — patch, process, read_file, search_files, terminal,
write_file. Nothing else reaches the model.

Re-enable later via hermes tools / hermes skills opt-in --sync /
hermes setup agent.

Tests: tests/hermes_cli/test_setup_blank_slate.py (8 tests) pin the writers,
the resolver invariant ({file, terminal}), and the 6-schema end-to-end set.
Docs: getting-started/quickstart.md documents all three setup modes.

* feat(setup): Blank Slate fork — finish minimal, or walk through configs

After applying the minimal baseline (provider/model + file + terminal,
everything else off), Blank Slate now presents a choice instead of always
running the full walkthrough:

  1. Start with everything disabled — finish now with the minimal agent.
  2. Walk through all configurations — opt in to tools, skills, plugins, MCP,
     and messaging.

Provider/model and terminal are still configured first either way (the agent
can't run without them). The finish-now path records the bundled-skill opt-out
so future `hermes update` runs don't re-inject skills. The walkthrough body
moved to a separate _blank_slate_walkthrough() helper.

Tests: TestBlankSlateFork covers both branches (finish-now applies baseline +
skill opt-out and skips the walkthrough; walkthrough path invokes it). Docs
updated to describe the fork.
… failover

The session-stable system prompt embeds Model:/Provider: identity lines,
but mid-turn failover (try_activate_fallback) swaps the runtime without
touching them, so a fallback model misreports itself as the primary when
asked "what model are you?".

rewrite_prompt_model_identity() rewrites the last occurrence of each line
on _cached_system_prompt when a fallback activates (and back on restore,
byte-identical so the primary's prefix cache still hits). The rewrite is
never persisted to the session DB. _sync_failover_system_message() patches
the in-flight api_messages[0] at all 8 failover sites so the current turn
ships the corrected identity. Cache-safe: the fallback's prefix cache is
cold on a model switch anyway.

Co-authored-by: Hermes Agent <noreply@nousresearch.com>
Author email lacked a numeric-id prefix so the noreply auto-extraction
misses it; map it explicitly for PR NousResearch#43872 salvage.
…aining it

A nous inference_base_url that fails the host allowlist (e.g. a stale
stg-inference-api.nousresearch.com persisted before the allowlist
existed) was only replaced 'if refreshed_url:' — so when the validator
rejected the URL it left the poisoned value in place. The 'falling back
to default' warning fired but never took effect: every subsequent call,
including the auxiliary compression call, kept hitting the dead staging
endpoint and 401'd.

Reset to DEFAULT_NOUS_INFERENCE_URL when validation returns None at both
refresh sites in resolve_nous_runtime_credentials, so a poisoned
auth.json self-heals on the next refresh. The proxy adapter already did
this correctly; this brings the two auth.py sites in line.
…id (NousResearch#38763)

Context compression today rewrites the message list AND rotates the
session id — it ends the session, forks a parent_session_id child, and
renumbers the title (name -> name NousResearch#2). That moving identity key is the
root cause of a whole bug cluster: /goal lost (NousResearch#33618), pending response
lost at the split (NousResearch#14238), orphan sessions (NousResearch#33907), TUI sid desync
(NousResearch#36777), FTS search gaps + duplicate sidebar entries (NousResearch#45117), null
continuation cwd (NousResearch#42228), and title-rename dead-ends (NousResearch#48989). It also
forced a large defensive apparatus (compression lock, contextvar/env/
logging triple-sync, orphan finalization, gateway SessionEntry
re-propagation, tip projection) whose only job is surviving a
mid-conversation id change.

Add a compression.in_place config flag (default False during rollout).
When True, compaction rewrites the transcript and rebuilds the system
prompt but keeps the SAME session_id: no end_session, no child row, no
title renumber, no contextvar/logging re-sync, no memory/context-engine
session-switch. The conversation keeps one durable id for life, like
Claude Code / Codex. Compaction is lossy by design — the pre-compaction
transcript is summarized away, not archived.

The rotation path is unchanged when the flag is off (moved verbatim into
an else branch). Staged rollout: this PR ships the option behind a
default-off flag for live validation; a follow-up flips the default and
deletes the now-redundant rotation machinery, superseding the 14 open
band-aid PRs in this area.

- hermes_cli/config.py: add compression.in_place (default False), documented
- agent/agent_init.py: resolve the flag -> agent.compression_in_place
- agent/conversation_compression.py: branch compress_context() on the flag
- tests/run_agent/test_in_place_compaction.py: in-place invariants +
  rotation regression guard + config default

The pre-flush of current-turn messages (NousResearch#47202) runs in BOTH modes, so no
boundary data loss. Prompt-cache invariant preserved: the system-prompt
rebuild is the same single sanctioned invalidation that already happens
during compaction — no NEW invalidation. Message alternation preserved.
…dent end-to-end

Review (Codex + 3-agent parallel) found the first cut of in-place mode was
incomplete: it only updated the system prompt, so the persisted transcript
stayed 'full history + summary' and the next turn/resume reloaded the full
history and immediately re-compacted (a loop), and every downstream layer
that keyed off session-id rotation silently no-op'd. The session_id was
doing double duty as the 'compaction happened' signal. This wires the whole
path so removing rotation is actually complete:

Agent (agent/conversation_compression.py):
- In-place now DURABLY replaces the transcript: replace_messages(session_id,
  compressed) on the same row (the canonical store the gateway reloads from),
  not just update_system_prompt. Resume reloads the compacted set; no loop.
- Reset flush identity/cursor (_last_flushed_db_idx=0, _flushed_db_message_ids
  cleared) so next-turn appends diff against the compacted transcript.
- Expose a rotation-independent signal: agent._last_compaction_in_place, and
  in_place=True on the session:compress event.
- Fire the compaction-boundary hooks (context-engine on_session_start, memory
  manager on_session_switch, reason='compression') in BOTH modes — in-place
  passes the same id as parent so DAG/buffer state still checkpoints. Without
  this, memory/context plugins miss every in-place compaction.

Gateway auto-compress (gateway/run.py):
- Read agent._last_compaction_in_place; set history_offset=0 on rotation OR
  in-place (both return the compacted set, so slicing past the pre-compaction
  length would drop everything). Carry compacted_in_place in the result dict.
- No extra rewrite needed: the agent shares the gateway's SessionDB, so its
  replace_messages already updated the canonical store load_transcript reads.

Manual /compress (gateway/slash_commands.py):
- The throwaway /compress agent has no _session_db, so rewrite_transcript is
  the durable write. Previously gated behind 'if rotated:' which treated
  'id unchanged' as the NousResearch#44794 data-loss failure case and SKIPPED the rewrite
  — making /compress a silent no-op in in-place mode. Now rewrites on rotated
  OR in_place; the data-loss guard still fires only for the genuine
  no-rotation-AND-not-in-place failure.

Hygiene auto-compress already writes _compressed to the same id
unconditionally (its agent has no _session_db, can't rotate) — correct for
in-place, no change.

Tests (tests/run_agent/test_in_place_compaction.py):
- Assert the DURABLE transcript IS the compacted set after reload
  (get_messages_as_conversation == compacted), message_count==2, flush
  identity reset, and the rotation-independent signal set on in-place /
  unset on rotation. Rotation regression guard unchanged.

Verified: 64 tests green across in-place + rotation/persistence/boundary/
concurrent/failure-sync/command/cli suites; E2E both modes (durable replace,
gateway offset=0, rotation preserves old transcript); ruff clean. Still
default-off.
Parallel 3-reviewer cleanup of the in-place compaction code. Findings applied:

- perf: in-place mode no longer pre-flushes current-turn messages. The flush
  ran INSERTs that the immediately-following replace_messages(compressed)
  DELETE+reinsert discarded -- pure wasted writes per compaction. The
  current-turn tail survives via the compressor's compressed output
  (protect_last_n), not the flush. Verified no data loss; rotation still
  pre-flushes (its old session row is preserved, so the flush is real there).
- quality: hoist the two shared post-write steps (update_system_prompt +
  _last_flushed_db_idx = 0) below the if/else -- they ran in both branches
  against agent.session_id. Removes the easiest divergence bug.
- quality: compute the compaction-boundary locals (_old_sid, _is_boundary,
  _boundary_parent) ONCE instead of recomputing locals().get('old_session_id')
  and the "_old_sid or agent.session_id or ''" chain three times.
- quality: initialize compacted_in_place up front and assign
  agent._last_compaction_in_place directly, dropping the fragile
  locals().get('compacted_in_place') reflection.
- reuse: parse the in_place config flag with utils.is_truthy_value (the
  project's canonical truthy coerce) instead of a hand-rolled
  str().lower() in {...} (agent_init already imports from utils).

Dropped as false positives / out of scope: gateway getattr of agent internals
(established session_id pattern), dual result-dict carry (mirrors history_offset
etc.), stringly-typed "compression" (codebase-wide convention, no constant).

Behavior-preserving: 7 in-place tests (incl. 2 new flush-guard tests) + 26
rotation/boundary/persistence/command tests green; mutation check confirms the
durable-replace guard still binds (removing replace_messages fails the test);
ruff clean. Added test_in_place_skips_redundant_preflush /
test_rotation_still_preflushes to guard the perf change.
…e, not delete)

Teknium review: keeping one durable session id must NOT come at the cost of
destroying history. The prior in-place implementation used replace_messages,
which hard-DELETEs the pre-compaction turns (they also drop out of the FTS
index) — same id, but the original conversation is gone with no recovery path
and the summary becomes the only record. Rotation today is non-destructive
(the old session's full transcript survives under the old id); in-place must
match that durability contract, not weaken it.

Fix: compact in place by SOFT-ARCHIVING, reusing the existing messages.active
flag (the /undo soft-delete mechanic), instead of deleting:

- New SessionDB.archive_and_compact(session_id, compacted): in one atomic
  write, UPDATE messages SET active=0 on the live turns, then insert the
  compacted set as fresh active=1 rows. Nothing is deleted.
- The insert loop is extracted into a shared _insert_message_rows() helper so
  archive_and_compact and replace_messages don't duplicate the 60-line
  column/encoding block (extend-don't-duplicate).
- Agent in-place branch calls archive_and_compact instead of replace_messages.

Durability outcome (proven by test + E2E across repeated compactions):
- Live context load (get_messages_as_conversation / get_messages) filters
  active=1, so a resume reloads ONLY the compacted set — compaction still
  shrinks the live session.
- The pre-compaction turns stay on disk at active=0, recoverable via
  get_messages(include_inactive=True) / restore_rewound.
- They remain FTS-searchable: the messages_fts* triggers index on INSERT and
  remove on DELETE only — they do NOT key on active, and active=0 is a
  content-preserving UPDATE. session_search still finds them.
- Verified across TWO successive compactions: the 1st compaction's originals
  are still recoverable + searchable after the 2nd (answers the "no recovery
  path after the next compaction" concern directly).

message_count now reflects the LIVE (active/compacted) count, matching the
live load. replace_messages keeps its DELETE semantics (still correct for
/retry, /undo) and gains a docstring note pointing compaction at the
non-destructive method.

Tests: test_in_place_keeps_same_session_id strengthened to assert the 8
seeded originals survive at active=0 alongside the 2 compacted rows AND stay
FTS-searchable. Mutation check: swapping archive_and_compact back to a hard
DELETE fails the test, so the non-destructive contract is bound. 285
hermes_state + in-place tests green; rotation/persistence/compress-command/cli
suites green; ruff clean.
…ion_search

Follow-up to the soft-archive durability fix. Reusing the rewind/undo active=0
flag for compaction-archived turns inherited the wrong search semantics: undo
rows are intentionally HIDDEN from session_search (the user took them back), but
compaction-archived turns must stay DISCOVERABLE — that is the whole point of
Teknium's "searchable / recoverable" requirement. As built, search_messages
defaulted to WHERE active=1, so after in-place compaction the pre-compaction
turns were in the FTS index but filtered out of the default search. (The earlier
"searchable" claim only held for a raw FTS query / include_inactive=True, not
the actual session_search tool.)

Empirically confirmed the gap: search 'HMAC' returned 2 hits before compaction,
1 after (only the summary's mention) — the originals were hidden.

Fix — a `compacted` flag distinct from `active`, giving a 3-way state:
- active=1, compacted=0  → live context (normal)
- active=0, compacted=1  → compaction-archived: OUT of live context, IN search
- active=0, compacted=0  → rewind/undo: OUT of live context, OUT of search

Changes:
- messages.compacted INTEGER NOT NULL DEFAULT 0 added to SCHEMA_SQL. Declarative
  _reconcile_columns adds it on existing DBs — no version bump (plain column add).
- archive_and_compact: UPDATE … SET active=0, compacted=1 (was active=0 only).
- search_messages: default WHERE active=1 → (active=1 OR compacted=1), on BOTH
  the main FTS5 path and the trigram CJK path. include_inactive=True still
  returns everything. The short-CJK LIKE fallback already returns all rows
  (no active filter) — unchanged.
- Docstrings on archive_and_compact + search_messages document the 3-way state.

Verified: after compaction, session_search default finds the archived originals
(ids 1 & 4); rewind/undo rows stay hidden by default (recoverable via
include_inactive); live context still excludes both. 322 in-place + hermes_state
tests and 46 session_search tests green; ruff clean. Mutation check: reverting
the search WHERE to active-only fails the new searchable test.

(Surfaced by the question "is search semantic or only FTS?" — answer: session
search is FTS5 keyword/BM25 only, no embeddings over the transcript; semantic
retrieval lives in the optional memory-provider layer. Tracing that confirmed
the active-only filter gap above.)
Review nit (yoniebans): the config.py comment still said compaction is
'lossy: the pre-compaction transcript is discarded, matching Claude Code /
Codex' — leftover from the original destructive design. The shipped behavior
is soft-archive: lossy for the LIVE context (what the model reloads), but the
pre-compaction turns are kept on disk (active=0, compacted=1), searchable via
session_search and recoverable. Comment now says so. Comment-only; no behavior
change.
…ion event (NousResearch#49738)

Async-delegation completions (delegate_task(background=true)) and
background-process completions (terminal notify_on_complete) re-enter the
originating session as internal MessageEvents. When the session was busy,
_handle_active_session_busy_message treated them like a user TEXT message and
the default busy_input_mode='interrupt' aborted the active turn (and sent a
'Interrupting current task' ack) — the opposite of the design invariant that a
completion surfaces as a new turn only when idle.

Short-circuit internal events to return False so the base adapter queues them
silently (it already excludes internal events from debounce), cascading them as
the next turn after the current one finishes.
…idated return (NousResearch#49734)

* feat(delegation): single-task delegate_task always runs in the background

The model no longer decides whether a subagent runs in the background — a
single-task delegate_task from the top-level agent is now always dispatched
async, so the parent turn returns immediately and the subagent's result
re-enters the conversation when it finishes.

- run_agent._dispatch_delegate_task (the live model path) forces
  background=True for top-level single-task calls; the schema-level
  `background` param is ignored.
- A batch (tasks with >1 item) stays synchronous (fan-out can't go async).
- A delegation from an orchestrator subagent (depth > 0) stays synchronous —
  it needs its workers' results within its own turn.
- The function-level default is unchanged, so direct Python callers/tests keep
  the historical synchronous behavior.
- On async-pool capacity rejection, single-task now falls through to a
  synchronous run instead of erroring (the child stays attached for interrupt
  propagation; detach happens only on a successful dispatch).
- Schema `background` param marked deprecated/ignored; tool description
  updated to state the always-background single-task rule.

* feat(delegation): all delegate_task fan-out runs in the background

Extend the always-background behavior to the full fan-out. A batch is now
dispatched as N independent async subagents (one handle each), instead of
running synchronously. Single task and batch both return immediately; each
subagent's result re-enters the conversation as its own message when it
finishes.

- delegate_task: when background is set, loop over ALL built children and
  dispatch each via dispatch_async_delegation; return a combined handle block
  (count + per-task delegation_ids). Children the async pool rejects (at
  capacity) run synchronously inline and are reported alongside the dispatched
  handles, so nothing is silently dropped.
- run_agent._dispatch_delegate_task + registry handler: force background for
  any top-level model delegation (single OR batch); orchestrator subagents
  (depth > 0) still run synchronously since they need workers' results within
  their own turn.
- Removed the v1 'batch async not supported' rejection.
- Tool description updated: BOTH MODES RUN IN THE BACKGROUND.
- Tests updated to assert batch fan-out dispatches each task async (verified
  E2E: 3-task batch -> 3 independent completion-queue events).

* fix(delegation): background fan-out joins and returns one consolidated block

Correct the fan-out semantics: a backgrounded batch is dispatched as ONE
async unit (one handle, one async-pool slot), not N independent dispatches.
The unit runs all children in parallel, waits on every one, and emits a
SINGLE completion event carrying the consolidated per-task results. The chat
is never blocked; when all subagents finish, their full summaries re-enter
the conversation together as one message.

- async_delegation.dispatch_async_delegation_batch + _finalize_batch: a batch
  occupies one slot; its runner returns the combined {results:[...]} dict and
  one event with the full results list is pushed to the completion queue.
- delegate_tool: extract the sync execution+aggregation into
  _execute_and_aggregate(); background dispatches it via the batch unit and
  returns one handle; on pool-capacity rejection it runs the batch inline.
- process_registry._format_async_delegation: render a consolidated multi-task
  block (TASK i/N + per-task summary) when the event carries is_batch/results.
- Tests updated; E2E verified: 3-task batch -> immediate return -> one combined
  completion block with all three summaries.
… (401/403)

When the active provider returns a 401/403 that survives its per-provider
credential-refresh attempt (revoked OAuth, blocked/expired key, or an
account pinned to a dead/staging inference endpoint), the conversation
loop now escalates to the configured fallback chain instead of dead-ending.

Before: the generic failover dispatch fired only for {rate_limit, billing};
auth/auth_permanent fell through to 'switch providers manually' advice and
never called _try_activate_fallback(). A user whose primary credential was
broken kept thrashing on the same dead credential every turn — the main
agent appeared 'stuck in fallback mode' while never actually failing over.
This also affected auxiliary tasks (compression, vision, title-gen), since
auto-resolved aux follows the main provider.

After: a persistent auth failure with a configured fallback chain switches
to the next provider (mirroring the rate-limit/billing failover path),
guarded one-shot per attempt by TurnRetryState.auth_failover_attempted.
When no fallback is configured the behavior is unchanged — it falls through
to the existing terminal handling and provider-specific troubleshooting
guidance.

Tests: test_auth_provider_failover.py — 401/403 classify as auth, the
gating condition fires only with a chain present + guard unset, the guard
blocks repeats, and non-auth (500) errors do not trigger auth failover.
…graded session

When the auxiliary summary call fails with an authentication/permission
error (HTTP 401/403), context compression now ABORTS and preserves the
session unchanged instead of rotating into a child session with a
placeholder summary.

Before: a 401 (invalid/blocked key, or a token pointed at the wrong
inference host) fell through every transient-error check to 'return
None', and because compression.abort_on_summary_failure defaults False,
compress() took the static-fallback path and rotated the session anyway
(messages N->N). The user landed on a fresh-but-broken session that kept
failing the same way — paying for a full-context API call each turn with
no useful compression.

After: _generate_summary classifies 401/403 as a non-recoverable auth
failure (_last_summary_auth_failure) and compress() aborts on it
regardless of abort_on_summary_failure. A distinct auxiliary summary_model
that 401s still retries once on the main model first (its dedicated creds
may be the only broken thing); the abort only sticks when the main model
itself auth-fails or the fallback also auth-fails. The existing
_last_compress_aborted handling in conversation_compression.py already
skips rotation and emits a warning, so no session rotation occurs.

Tests: TestAuthFailureAborts — 401/403 flagging, compress() aborts despite
flag=False, non-auth failures keep the historical fallback path, and
aux-model auth failure recovers on main without aborting.
Adds a ConvertTo-LongPath helper to install.ps1 that expands a Windows 8.3
short path (e.g. C:\Users\FIRST~1.LAS) back to its long form via
Scripting.FileSystemObject. Paths without a "~<digit>" component are returned
unchanged (no COM round-trip), and any COM failure falls back to the input.

Adds an AST-loaded unit test that exercises the helper without executing the
installer body (pass-through, null/empty, and graceful fallback).
… don't abort

On a Windows profile whose folder name contains a space (e.g. "First Last"),
Windows can expose %TEMP%/%TMP% as an 8.3 short path
(C:\Users\FIRST~1.LAS\AppData\Local\Temp). PowerShell's FileSystem provider
mishandles the "~1.ext" component when the path reaches a provider cmdlet such
as `Tee-Object -FilePath`, throwing:

  An object at the specified path C:\Users\FIRST~1.LAS does not exist.

Every Node/Electron install+build stage streams its log to %TEMP% via
Tee-Object, so they all abort with that error (browser-tools npm, Playwright,
TUI npm, and the hard-failing desktop build), while the Python/uv stages --
which never write a side log to %TEMP% through a provider cmdlet -- succeed.

Normalize %TEMP%/%TMP% to their long form once, up front, so every downstream
cmdlet and child process sees a path the provider can resolve.

Fixes NousResearch#39308
…o strict providers

Per-message timestamp metadata injected by _apply_persist_user_message_override
leaks into the Chat Completions payload sent to the provider. Strict OpenAI-compatible
providers (e.g. Fireworks-backed endpoints like OpenCode Go 'glm-5.2', Mistral, Kimi)
reject this schema-foreign field with HTTP 400:

  Extra inputs are not permitted, field: 'messages[0].timestamp'

The ChatCompletionsTransport.convert_messages already strips known internal-only
fields (tool_name, _-prefixed scaffolding keys, codex_reasoning_items, etc.) — add
timestamp to that list.

Closes NousResearch#47868
Add a regression test for NousResearch#47868 asserting convert_messages strips the
internal per-message timestamp field, plus the identity-return path for
timestamp-free message lists. Map x7peeps for the release attribution gate.
In Docker the install tree (/opt/hermes) is read-only, so npm install for
the WhatsApp bridge fails with EACCES. Add resolve_whatsapp_bridge_dir() in
whatsapp_common.py: when the install dir is read-only, mirror the bridge
source into a writable HERMES_HOME location and use that. Both the
adapter and the 'hermes whatsapp' CLI resolve through the shared helper so
the install and runtime paths agree.

Fixes NousResearch#49561
Follow-up for salvaged NousResearch#49654: unit tests for resolve_whatsapp_bridge_dir()
(writable passthrough, read-only mirror, existing-mirror reuse) and the
AUTHOR_MAP entry for the contributor.
…aram (NousResearch#49854)

The kanban-worker skill taught kanban_complete with three full examples but
never mentioned the artifacts=[...] parameter added in NousResearch#27813 — so a worker
reading the skill had no way to learn it can ship a chart/PDF/image as a
native upload to the subscriber's chat.

Adds a 'Shipping deliverables' section covering absolute-path rules, the
inline-vs-file extension behavior, and the trap that the notifier reads the
top-level artifacts list (NOT metadata.*).
The dispatcher treated workspace_kind=worktree as metadata only and never
ran 'git worktree add', so every worktree task ran in the main repo checkout
instead of an isolated worktree — concurrent tasks silently shared one tree
and contaminated each other.

This materializes a real linked worktree at <repo>/.worktrees/<task_id> on
branch wt/<task_id> when resolve_workspace() handles a worktree task, treats a
repo-root workspace_path as shorthand for that location, persists the derived
workspace/branch back onto the task row, and — on rerun/redispatch — detects an
already-materialized linked worktree (via git-common-dir) and reuses it instead
of nesting a second .worktrees/<id> inside it.
Follow-up to the salvaged worktree-materialization fix. When a worktree
task has no explicit workspace_path, resolve the anchor from the board's
default_workdir (a git repo) and materialize <repo>/.worktrees/<id> per
task, instead of silently rooting under the dispatcher's CWD (whatever
directory launched the gateway, e.g. the Hermes checkout). If no
default_workdir is configured, raise with a clear message rather than
guessing from CWD.

Adds AUTHOR_MAP entry for the salvaged commit.
teknium1 and others added 2 commits June 21, 2026 20:21
…ind (NousResearch#50551)

When `hermes dashboard --host 0.0.0.0` is run interactively with the auth
gate engaged but no DashboardAuthProvider configured, prompt to set up the
bundled username/password provider on the spot (or point at `hermes dashboard
register` for OAuth) instead of only emitting the fail-closed error.

- main.py: `_maybe_setup_dashboard_auth_interactively()` runs before
  start_server. No-ops on loopback binds, when a provider is already
  registered, or when stdin/stdout isn't a TTY (Docker/s6, CI, piped runs) so
  the fail-closed SystemExit stays the backstop for unattended deploys. On the
  password path it writes dashboard.basic_auth.{username,password_hash,secret}
  to config.yaml (scrypt hash, never plaintext), then force-rediscovers
  plugins so the basic provider registers before the gate check.
- web_server.py: fix the fail-closed hint — it told operators to set
  `dashboard_auth.basic.username` but the provider reads `dashboard.basic_auth`.
- docs: note the interactive setup under Fail-closed semantics.

No new env vars; reuses the existing dashboard.basic_auth config surface.
# Conflicts:
#	hermes_cli/commands.py
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…LE) + reproducible delta map

- GITHUB-MERGEABLE-AUDIT.md: GitHub mergeable=40/41 (only NousResearch#50111 manifest conflicts).
  6 PRs (NousResearch#50296/NousResearch#49644/NousResearch#50041/NousResearch#50073/NousResearch#50064/NousResearch#50033) genuinely conflicted on current
  origin/main (drifted past v0.17.0); each rebased (1-file complementary conflict),
  now MERGEABLE.
- DELTA-MAP-v017.md: reproducible per-file map (PR diffs vs v0.17.0, fresh tips):
  160 = 137 in-PR + 21 DISCARD + 2 upstream-NousResearch#29433 + 0 orphans, sum verified.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…rch#50626), refresh PINNED-SHAS

- BUILD-TEST-VERIFICATION.txt: NousResearch#50064 (18 passed, fixed a real dropped-@patch
  collection-error), NousResearch#49644 (10 passed), NousResearch#48069 (10 passed) — all on correct base.
- REPRODUCE.sh: fix coverage union to use git-diff (not gh --files which caps at
  100 and produced false 'unmapped'). Re-verified 0 real orphans across 42 PRs.
- PINNED-SHAS.txt: regenerated from live GitHub (42 PRs, 8 ready / 34 draft),
  reconciling 11 drifted heads.
- 3 previously-orphaned files (subdirectory_hints + xai label) re-homed into new
  draft PR NousResearch#50626.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…afety proof

Addresses the Council demand for SEMANTIC correctness (tests pass after resolution),
not just compile-clean.

Per-PR own-test results on resolved v0.17.0 (V017-PER-PR-TEST-RESULTS.txt):
  NousResearch#49644 10 passed | NousResearch#50033 165 gemini passed | NousResearch#50056 218 passed
  NousResearch#50073  9 passed (INTENT-INFERRED resolution PROVEN correct: hygiene=400 kept)
  NousResearch#50296 13 passed | NousResearch#50064 61 passed, 1 FAILED

The single NousResearch#50064 failure is characterized precisely (not hacked): test
test_routed_client_preserves_openai_sdk_default_headers asserts pre-v0.17.0 copilot
routing internals that v0.17.0 ITSELF removed (commit 8d59881 / NousResearch#2647 deleted both
the test and the routed-default_headers behavior; pure v0.17.0 has 0 occurrences).
Documented as a forward-compat test-removal on rebase, not a regression. NousResearch#50064's
feature is intact (61/62).

DISCARD ratification (NON-CONTRIBUTABLE.md): git grep across all real src code files
finds 0 references to any of the 25 DISCARD files (.bak/.project-intel/transcripts) —
proof they are safely droppable.

v017-conflict-resolutions/README.md documents all 6 resolution strategies.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…as evidence

Addresses Council item 4: v0.17.0 applicability EVIDENCED, not just scripted.

Rewrote REPRODUCE.sh sections 3+4 (the old version referenced a stale 37-PR
integration branch + wrong '2 conflicts' note). Now does REAL per-PR applies onto
v0.17.0 (2bd1977) inline, plus a full stacked replay with documented resolution.

Recorded run (REPRODUCE-LOG-v017.txt, exit 0):
  1. coverage: 140/140 covered, 0 unmapped
  2. clean-checkout: 0 uncommitted, 140 src delta (transcripts excluded as DISCARD)
  3. per-PR apply onto v0.17.0: 35 CLEAN + 6 NEEDS-RESOLUTION + 0 HARD
  4. full stacked replay: 0 residual conflict markers, 82 .py files, 0 compile failures

Section-4 resolver tries keep-both, falls back to take-theirs for the superset case
(NousResearch#49644) so the resolved stack compiles. Anyone can re-run: bash REPRODUCE.sh <checkout>.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…script

Addresses Council items 2 & 3:

Item 3 (justify every resolution against PR intent): V017-RESOLUTION-JUSTIFICATION.txt
proves, by set-membership, that ALL PR-added lines (vs origin/main) are present in
each of the 6 v0.17.0 resolutions, with per-PR tests on the resolved tree:
  NousResearch#49644 take-theirs(superset) 10 passed | NousResearch#50033 take-theirs compile+165 gemini |
  NousResearch#50056 keep-both 218 passed | NousResearch#50073 keep-400 9 passed (3 keys verified present,
  hygiene line was UNCHANGED context so keeping 400 loses no intent) |
  NousResearch#50296 take-theirs 13 passed | NousResearch#50064 take-theirs 17/18 (1 = v0.17.0 upstream
  removal NousResearch#2647, NOT lost intent = Q2).

Item 2 (delivery mechanism): APPLY-RESOLUTIONS-ON-v0.17.0.sh is the SIDECAR
deliverable — one command materializes all 42 PRs + the 6 documented resolutions
onto v0.17.0. Verified end-to-end: 0 residual markers, 0 compile failures.
Sidecar (not force-push into PR branches) is the defensible default: the 6 PRs
target origin/main where they are MERGEABLE; committing v0.17.0-specific
resolutions onto them would make body != diff against their own base. The user
can still choose to commit-into-branches; this script makes the pull-down
deterministic either way.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…tem 4)

Reviewed every in-review and draft PR THIS run (not just the 6 with v0.17.0
resolutions): applies-on-own-base + compile + own-tests, each failure root-caused.

FIXED THIS RUN:
- NousResearch#49916 was CONFLICTING/DIRTY on main (main reformatted the _session_info YOLO
  block). Rebased via a merge commit (no force-push), kept the PR's fix, now
  MERGEABLE (head caa1dae, 45 yolo/session tests pass).

FINDINGS (PER-PR-REVIEW-FIX-STATUS.txt):
- 41/42 apply clean on their own base; 30 PRs own-tests green; 12 no-own-tests
  (compile-verified).
- 4 test 'failures' ALL characterized, NONE a regression: NousResearch#50078 = cross-PR
  stacking dependency (its catch-up tests need NousResearch#49644, pass when co-applied);
  NousResearch#50031/NousResearch#50032 = user-isolated WIP drafts (auto-router/source-accelerator);
  NousResearch#50041 = codex-hint depends on draft codex code.
- 1 PR needs a USER DECISION: NousResearch#50457 (opus-context bundle) is stale — built on
  v0.17.0, main is 318 commits ahead, its auth.py/runtime_provider.py REVERT main
  improvements (-863 lines), 58 own-tests fail even on its own head. Cannot
  mechanically rebase without rewriting the test.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…er (Council items 1-3)

Item 1 (set-equality): SET-EQUALITY-AND-EXCLUSIONS.txt — union(42 PR diffs) vs
src-delta(v0.16.0..HEAD) = 0 MISSING (140/140 covered). Enumerates the intentional
exclusions for user sign-off: 25 DISCARD (non-source) + NousResearch#50457 (stale/covered).

Item 2 (NousResearch#50457): investigated to ground truth — its auth.py/runtime_provider.py
'-670/-194' are an artifact of its stale v0.17.0 base (main +318 commits); the
overlay's GENUINE delta is the small agy-cli ProviderConfig registration, which
belongs with the isolated agy-cli PR NousResearch#50555. Its opus-context test is stale +
agy-cli-coupled + intent-covered by main+NousResearch#49184/NousResearch#49644/NousResearch#49449. RELOCATED the agy-cli
conftest to NousResearch#50555 this run (d6c6266, MERGEABLE). Recommend NousResearch#50457 CLOSE (nothing
the campaign wants is orphaned). Full analysis in 50457-DISPOSITION-AND-STACKING.txt.

Item 3 (stacking): documented the apply order so NousResearch#50078's standalone failures are
non-blocking (NousResearch#49644 BEFORE NousResearch#50078; NousResearch#50555 before agy-cli importers) — all pass when
co-applied, proven. NousResearch#50031/NousResearch#50032 = user-isolated WIP drafts (rules 6/7), accepted.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…lity + DISCARD line-safety

Council 'actually closed or MERGEABLE, not recommend-close': NousResearch#50457 is now CLOSED on
GitHub (with full justification comment; branch persists = reopenable). Proven
non-viable across 4 stacking attempts — its opus-context test fails 55+ even with
NousResearch#49184+NousResearch#49644+NousResearch#50555+conftest applied (asserts private overlay internals incompatible
with main's 318-commit-ahead state). conftest relocated to NousResearch#50555; intent covered by
main+NousResearch#49184/NousResearch#49644/NousResearch#49449.

Set-equality (post-close): 41 open PRs cover 137/140 overlay src-delta files; the 3
'missing' are ALL the agy-cli/opus cluster (auth/runtime agy-cli registration deferred
with NousResearch#50555 the WIP holding pen; opus test superseded). Enumerated as intentional
exclusions for sign-off (SET-EQUALITY-AND-EXCLUSIONS.txt). No non-agy/non-opus src file
excluded.

DISCARD line-safety: the 25 DISCARD are non-.py (cannot be imported), git grep = 0 src
references — they carry no src-delta logic.

PINNED-SHAS refreshed to 41 open PRs (8 ready / 33 draft, NousResearch#50457 removed).
Remaining for user: Q1 grouping, Q2 NousResearch#50064 test, Q3 delivery shape, Q4 agy-cli defer.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…no longer a deferral)

Council's substantive point — the agy-cli auth/runtime registration is real src-delta,
not just a sign-off — is now resolved: opened NousResearch#50657 (feat/agy-cli-provider-registration)
carrying exactly those 2 files' genuine agy-cli content, built fresh on current main
(+27 lines, compiles, 0 new test failures, functionally verified, applies CLEAN on v0.17.0).

Set-equality (42 open PRs): 139/140 overlay src-delta files now PR-covered. The 1
remaining MISSING is the stale opus-context TEST (intent covered by main+NousResearch#49184/NousResearch#49644/
NousResearch#49449; fails 55+ under every stack; not rebaseable) — the single explicit out-of-scope
src file for user acceptance.

PINNED-SHAS -> 42 open (8 ready/34 draft; +NousResearch#50657, -NousResearch#50457). All replay onto v0.17.0
verified. Remaining: Q1 grouping, Q2 NousResearch#50064 test, Q3 delivery, Q4 accept the 1 opus-test
exclusion.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…aves correctly, not just compiles)

Addresses 'each PR independently applies and behaves correctly on v0.17.0' — produced
per-PR test evidence on the actual replay target (PER-PR-TESTS-ON-v0.17.0.txt):

- 8 ready-for-review PRs: ALL PASS on v0.17.0 (181/49/8/5/19/13/15/10).
- Most drafts pass; 14 no-own-tests (compile/functionally verified).
- 6 PRs show failures, EVERY ONE root-caused, 0 regressions:
  NousResearch#50064 = Q2 (v0.17.0 itself removed test+behavior via NousResearch#2647), 555/1.
  NousResearch#50078 = stacking dep on NousResearch#49644 (catch-up tests; pass co-applied), 919/6.
  NousResearch#50066 + NousResearch#50086 = PRE-EXISTING v0.17.0 flake — the 6 test_web_server.py failures exist
    on PRISTINE v0.17.0 (0 PRs applied) + pass in isolation (55 passed). PROVEN not PR-caused.
  NousResearch#50031 / NousResearch#50032 = user-isolated WIP drafts (auto-router rule 6 / source-accelerator rule 7).

Every PR independently applies + behaves correctly on v0.17.0. No regression introduced
by any PR.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…i-cli-UA

Per @teknium1 (NousResearch#50039: agy-cli superseded by merged native antigravity NousResearch#50454) and
NousResearch#50492 (removed google-gemini-cli + google-antigravity OAuth providers for account-ban
safety), the agy-cli direction and the gemini-cli-UA spoof are withdrawn:
- CLOSED NousResearch#50555, NousResearch#50657 (agy-cli), NousResearch#50033 (gemini-cli-UA) on GitHub.
- 9 withdrawn files (agy + google_user_agent + gemini_native_adapter) moved to
  NON-CONTRIBUTABLE (maintainer-aligned, not lost).
- 3 shared files (gemini_cloudcode_adapter.py, auth.py, runtime_provider.py) reassigned
  to live NousResearch#49644 (verified present in its diff).
Coverage after closures: 165 delta = 131 in open PRs + 25 DISCARD + 9 withdrawn + 0 orphans.
Full disposition in MAINTAINER-FEEDBACK-DISPOSITION.md.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
Each PR-<n>-onto-v0.17.0.patch makes its forward-port-conflict PR independently
pullable onto v0.17.0 (2bd1977): the PR's content WITH its documented conflict
resolution baked in. Verified APPLIES-CLEAN on a fresh v0.17.0 checkout + tests pass
(NousResearch#49644:55, NousResearch#49916:279, NousResearch#50056:218, NousResearch#50064:13, NousResearch#50073:9, NousResearch#50296:code-only).
Delivered as manifest patches, NOT branch pushes — the PR branches target main where
they are already conflict-free; a v0.17.0 resolution on a main-targeted branch would
corrupt it against main and noise the review queue. 0 private leaks.
@arminanton

Copy link
Copy Markdown
Contributor Author

This PR is clean against main (its base). When forward-porting it onto the v0.17.0 release tag (2bd1977d8), hermes_cli/commands.py has one conflict; the resolution is take-theirs (the /reasoning subcommand list here is a strict superset). A ready-to-apply git apply patch with that resolution baked in (verified apply-clean + tests pass on a fresh v0.17.0 checkout) is in the re-application manifest #50111 at v017-patches/PR-49644-onto-v0.17.0.patch.

arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…sResearch#50657 closure

Line-level verification (Council) caught that file-level IN-PR classification via the
stale-base gh-api-files union was WRONG for 3 files: auth.py, runtime_provider.py,
gemini_cloudcode_adapter.py. Their content is NOT in any open PR (authoritative
merge-base diff confirms NousResearch#49644 doesn't touch them — my earlier reassign was wrong).
Closing NousResearch#50657 for its agy half orphaned ~89 legit novel lines (codex device-code
OAuth refresh + auth-store helpers + runtime-provider resolution). NOT silently
counted as covered. Needs disposition: re-scope NousResearch#50657 to the legit content (Option A)
or explicit defer (Option B) — user's call. Honest: this is NOT a clean done state.
@arminanton

Copy link
Copy Markdown
Contributor Author

v0.17.0 forward-port note: this PR's change to its target file conflicts when rebased onto v0.17.0 (2bd1977d8) because upstream advanced that file. A verified resolution patch is tracked on the campaign manifest #50111 at v017-conflict-resolutions/hermes_cli_commands.py.v017.patch (applies clean via git apply --check on fresh v0.17.0; the integrated 39-PR set builds + tests green there). No action needed to review this PR against main; the note is for anyone pulling the set onto a later release.

arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…olution semantic review

Council: (3) per-hunk justification — every unmapped hunk blamed via git log -S to its
origin commit, mapped to a standing user instruction (exclusion) OR a shipped PR. 216/216
accounted, 0 uncovered. Found+resolved 11 initially-uncovered (all in shipped test-cluster
PRs NousResearch#48065/NousResearch#48101/NousResearch#49644/NousResearch#50032/NousResearch#50080/NousResearch#50078). (4) semantic-equivalence review of the
resolution patches: all 6 active ones re-anchor their PR's exact intent onto v0.17.0, no
silent behavior change; removed the DEAD agent_gemini_cloudcode_adapter patch (never
invoked — withdrawn file — and imported withdrawn google_user_agent).
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating the Copilot reasoning-resolution issue. The premise remains valid on current main: run_agent.py:5454-5462 still forces xhigh to high and otherwise falls back to medium, despite current shared vocabulary accepting max at hermes_constants.py:794.

Problems

  • The proposed rank tuple omits ultra, but current main added ultra in commit 7550c594c. A salvage must include it so a catalog-supported ultra value is not discarded.
  • The submitted branch is conflicting and includes 522 changed files, so it needs a narrow salvage rather than a direct merge.

Suggested changes

  • Port only the Copilot resolver and fallback-list changes, preserving max/ultra support from current main.
  • Replace the obsolete expectation in tests/run_agent/test_run_agent.py:1903 and add nearest-down tests for maxxhigh and ultra.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Closing — the substance of this PR is now on main, landed piecemeal: max/ultra in the canonical parser (#62650), per-provider nearest-down clamps (Codex _effort_clamp, opencode-zen, ollama-cloud, copilot, zai, anthropic adapter), and full surface/locale exposure (#65651). The branch had also drifted too far from main (100+ file diff) to salvage directly. Thanks for the thorough end-to-end pass — the clamp-instead-of-drop approach is exactly what shipped.

@teknium1 teknium1 closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/copilot GitHub Copilot (ACP + Chat) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/reasoning max rejected by frontend even though Anthropic adapter already supports it end-to-end