feat(reasoning): accept "max" reasoning effort end-to-end with nearest-down clamp - #49644
feat(reasoning): accept "max" reasoning effort end-to-end with nearest-down clamp#49644arminanton wants to merge 260 commits into
Conversation
…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.
…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
…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.
…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.
…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.
…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>.
…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.
…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.
…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.
…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.
…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.
…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.
…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.
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.
|
This PR is clean against |
…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.
|
v0.17.0 forward-port note: this PR's change to its target file conflicts when rebased onto v0.17.0 ( |
…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).
|
Thanks for isolating the Copilot reasoning-resolution issue. The premise remains valid on current Problems
Suggested changes
Automated hermes-sweeper review. |
|
Closing — the substance of this PR is now on main, landed piecemeal: |
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:maxwas not in the effort vocabulary — settingreasoning_effort: max(or/reasoning max) silently fell back to the default.xhigh→highunconditionally (a stale guard from the free-tier deployment), and for any other unsupported level it snapped tomedium. So a high-effort request on a model whose real ceiling isxhighcollapsed all the way tomedium.Fix
hermes_constants.VALID_REASONING_EFFORTSgainsmax;parse_reasoning_effortdocuments 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 (somax→xhighfor GPT-5.x), falling back to the weakest only when nothing is at/below. Downgrades are logged at DEBUG instead of happening silently.maxthreaded through every remaining validation gate so it doesn't no-op at any layer: thechat_completionstransport gate (which would otherwise resetmax→mediumbefore the request ships), the/reasoningslash handlers + subcommand list (CLI and gateway), the effort-selection ordering, the config-loader docstring, andbatch_runner's--reasoning_effortvalidation.hermes_cli.models.COPILOT_REASONING_EFFORTS_GPT5gainsxhighso 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) —max→xhighfor GPT-5.x,maxhonored when supported,xhighno longer force-downgraded, nearest-down selection, weakest-fallback.ruffclean; 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/completionsroute 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
maxconstant but not the clamp logic that makesmaxactually resolve to a usable level on GPT-5.x. Fixes #29248.