Commit Graph

254 Commits

Author SHA1 Message Date
Teknium
1ee524f77d fix(memory): bind the checkpoint gate to post-turn micro-compaction too
Independent review caught a compaction authority the gate missed:
post-turn micro-compaction (turn_finalizer -> _micro_compact) absorbs the
oldest exchanges into a rolling summary with no pre-compress checkpoint
hook in its path, and both compression.checkpoint_required and
compression.micro_compact could be enabled together — assistant evidence
could vanish into a summary the checkpoint filter later excludes, without
ever reaching the durable provider.

- agent_init: checkpoint_required forces micro-compaction off (warned),
  mirroring the native-compaction suppression
- turn_finalizer: defense-in-depth guard at the call site (attribute is
  plain mutable state a future path could flip on a live agent)
- behavioral regression test with a sabotage control (gate off proves the
  harness reaches the call site; gate armed proves zero calls)
- docs + config example mention the suppression; stale v1 test header fixed
2026-08-25 03:55:55 -07:00
Teknium
9e551d2931 refactor(memory): renumber checkpoint API — v1 is the implicit historical contract, v2 opts into fail-closed checkpoints
Per review: existing providers should not be retroactively re-versioned or
handed a changed payload. Version 1 is now the implicit historical
on_pre_compress() contract (best-effort, raw message list) that every
pre-existing provider is already on; the fail-closed checkpoint contract
becomes version 2. MemoryManager routes the raw transcript to v1 providers
unchanged and hands the host-normalized evidence list only to v2+ checkpoint
providers, so the plugin surface contract for shipped providers is
byte-identical with the gate off.
2026-08-25 03:55:55 -07:00
Jan-Stefan Janetzky
8cc379b528 fix(memory): bind the checkpoint gate to every compaction authority
The fail-closed gate lived only in compress_context(), but two native
lossy owners compact without ever crossing it (review on #93996):

- codex app-server: in "native"/"off" auto-compaction mode (native is the
  default) Hermes preflight is skipped and the codex agent compacts its
  own thread inside run_turn() — the compress_context() rejection was
  unreachable. init_agent now refuses checkpoint_required together with
  api_mode=codex_app_server (BLOCKED_MISSING_PREREQUISITE, extracted as a
  testable guard), and run_codex_app_server_turn() fails closed as
  defense in depth before a turn can reach the codex-owned boundary.
- Responses server-side native compaction:
  native_compaction_context_management() now returns None while the gate
  is armed, so context_management never goes on the wire and the
  checkpoint-aware Hermes compressor stays authoritative. The suppression
  is logged once per process, not silently applied.

Regressions: checkpoint_required + app-server raises before run_turn()
(the session is never created); checkpoint_required keeps
context_management off the wire while the plain configuration still
produces it; the init guard refuses exactly the incompatible pair. Docs
and cli-config.yaml.example describe both bindings.

Refs #93986
2026-08-25 03:55:55 -07:00
Jan-Stefan Janetzky
1104ffe0b9 feat(memory): opt-in fail-closed pre-compress checkpoint contract (API v1)
Context compression is intentionally lossy. Deployments that archive
transcript evidence to an external durable store before compaction had no
way to guarantee the archive actually happened: MemoryManager.on_pre_compress
swallows provider failures by design, so a failed archive silently degraded
into data loss.

This adds an opt-in, provider-agnostic checkpoint contract:

- memory_provider: PRE_COMPRESS_CHECKPOINT_API_VERSION = 1; providers opt in
  by advertising pre_compress_checkpoint_api_version. Version 0 keeps the
  historical best-effort hook semantics.
- memory_manager: supports_pre_compress_checkpoint() capability probe;
  on_pre_compress(require_checkpoint=True) propagates checkpoint-provider
  failures and raises when no capable provider completed the checkpoint.
- conversation_compression: new compression.checkpoint_required config key
  (default false, documented in cli-config.yaml.example). When enabled,
  compaction fails closed with BLOCKED_MISSING_PREREQUISITE (the
  uncompressed transcript is preserved) unless a checkpoint-capable provider
  confirms the durable checkpoint. Providers receive normalized direct
  user/assistant evidence: tool rows, system messages, tool-call wrappers,
  and prior compaction summaries are filtered host-side into one stable
  contract. codex_app_server compaction is rejected under the gate because
  it exposes no truthful pre-compaction transcript boundary.
- hermes_state: persistent _compressed_summary column (declarative schema
  migration via _reconcile_columns) so summary provenance survives process
  restarts; only the resume model history carries the marker, keeping
  get_messages_as_conversation on its existing contract.
- gateway: the lossy hygiene/auto-compact paths load the memory provider
  (skip_memory=False) so a required checkpoint also guards those rewrites.

The gate arms only on an explicit boolean True (bare-MagicMock agents in
existing tests have truthy auto-attributes). Default behavior is unchanged:
checkpoint_required=false preserves best-effort semantics for all existing
providers. Contract tests, including a restart round-trip of the summary
marker, in tests/agent/test_pre_compress_checkpoint_contract.py.

Refs #93986
2026-08-25 03:55:55 -07:00
Leegenux
6ce7ab8bfb feat(browser): make snapshot threshold configurable 2026-08-24 21:51:44 -07:00
Teknium
47d6ce78a2 fix(tui): make startup_orphan_reap recoverable and move its config onto dashboard.*
Follow-up to the #65422 salvage:

- startup_orphan_reap joins _RECOVERABLE_END_REASONS (kept distinct from
  ws_orphan_reap for forensics): every recovery fence
  (find_latest_gateway_session_for_peer, unarchive_recoverable_session,
  promote_to_session_reset) now treats a startup-swept row as an
  accidental end, so a sweep never makes a session unresumable.
- Config key moves from sessions.orphan_reaper to
  dashboard.startup_orphan_sweep in DEFAULT_CONFIG, next to its siblings
  ws_ping_interval / ws_ping_timeout / ws_orphan_reap_grace_s; the raw
  loader in tui_gateway.server reads the new key (fail-open on missing).
  cli-config.yaml.example and website/docs/user-guide/configuration.md
  follow the dashboard.* documentation pattern.
- New regression test: a stranded 'active' row (ended_at NULL, no live
  runtime) is swept AND still recoverable via peer-keyed lookup and fully
  revivable via reopen_session afterward.
2026-08-23 18:58:40 -07:00
halaprix
d3e4b50e68 fix(tui): sweep orphaned tui/desktop/subagent session rows at gateway startup
Close session rows left ended_at IS NULL when the in-process websocket
orphan timer dies with the process (#65194). Dual-clock staleness
(started_at AND newest message), desktop included, live in-memory
sessions excluded, scheduled once from both entry.main and the WS
sidecar so desktop/dashboard boots also run the sweep.
2026-08-23 18:58:40 -07:00
abundantbeing
d524cc9a16 fix(browser): harden extension controller routing
Keep extension control opt-in and preserve existing browser backends unless an exact server-bound controller is available. Centralize protocol and capability admission across API and dashboard transports, make selected-controller results authoritative, bypass stale availability caches only inside bound requests, and serialize structured results for the existing tool contract.

Add a real browser_snapshot route-table/WebSocket E2E, strict admission and ownership regressions, public configuration and protocol documentation, and tests proving feature-off/no-controller compatibility.
2026-08-21 22:33:45 +05:30
MindDragonLabs
5c03fbedc6 fix(config): recognize memory nudge interval 2026-08-20 11:36:54 +05:30
Teknium
09e657793e feat: MCP tool results spill at 50K and carry upstream-elision warnings
Composio-style MCP servers return un-paginated 22-47K-char payloads that
sail under the generic 100K per-result spillover threshold, bloating
context and ballooning per-turn reasoning time on long conversations.
Competitors cap harder (OpenCode/pi 50KB, Claude Code 30K, Codex ~10K
tokens). Three changes:

- mcp_* tools spill at a tighter 50K default (BudgetConfig.mcp_result_size,
  config-overridable via tool_budget.mcp_result_size_chars; pinned and
  per-tool overrides still win; capped by the context-scaled default).
- The persisted-output preview now teaches recovery: page the saved file
  with read_file or process with execute_code instead of re-requesting the
  same data from the remote API.
- Untrusted/MCP string results are scanned (bounded, first 64KB) for
  provider-side elision markers ('...N more items', "has_more": true,
  'saved to sandbox', data_preview) and get ONE cache-safe incompleteness
  notice appended at result-construction time, before untrusted wrapping —
  so the model stops treating provider-elided enumerations as complete.
- Hard 2M-char allocation cap in mcp_tool.py (text, error, and
  structuredContent paths) so a pathological multi-MB server payload is
  bounded before it propagates, while ordinary large results reach
  spillover intact. Distilled from #56060/#56072/#56511 (issue #56059);
  supersedes their 50K lossy truncation with spillover-friendly semantics.

Docs: configuration.md spillover-budget section + cli-config.yaml.example.

Co-authored-by: Stoltemberg <215755014+Stoltemberg@users.noreply.github.com>
Co-authored-by: AlexFucuson9 <295703459+AlexFucuson9@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
2026-08-19 16:31:16 -07:00
Teknium
f2f6dce785 feat(providers): flip bundled meta-ai profile to the Responses wire
Reconciles the salvaged Responses-API mandate with the bundled meta-ai
plugin that landed in #88565:

- meta-ai profile api_mode -> codex_responses (prompt caching engages
  only on /v1/responses; 0% vs 93-99% measured). Custom endpoints with a
  non-api.meta.ai base URL still fall through to chat_completions via
  the host-driven mandate design.
- cli-config.yaml.example: point the example at MODEL_API_KEY (Meta's
  documented env var) and note the bundled provider covers the default
  endpoint
- tests/providers/test_meta_ai_profile.py updated for the new wire
2026-08-17 12:58:51 -07:00
Lavie
24545418eb fix(providers): route api.meta.ai through Responses API for prompt caching
- hermes_cli/providers.host_mandated_api_mode: add exact-hostname clause for
  api.meta.ai → codex_responses (measured 0% cache on /chat/completions vs
  93-99% on /responses with retention); update docstring.
- hermes_cli/runtime_provider._detect_api_mode_for_url: mirror clause for
  api.meta.ai (exact hostname, #32243) to keep runtime resolver in lockstep.
- agent/agent_init: call host_mandated_api_mode early in api_mode cascade
  (after explicit api_mode wins, before provider-name specials) via lazy
  import; single source of truth, preserves user override.
- agent/transports/codex._default_prompt_cache_retention_for_request: return
  24h for api.meta.ai unconditionally; build_kwargs setdefault preserves
  override; Bedrock branch untouched.
- cli-config.yaml.example: add commented providers.meta example (api_mode
  auto-detected).
- website/docs/developer-guide/adding-providers.md: list Meta alongside
  Codex/xAI as codex_responses native provider with retention note.
- tests: add hermetic behavior-contract suites for mandate, retention,
  content-addressed prompt_cache_key, reasoning passthrough, AIAgent init,
  usage cache reporting, model-switch override, and config roundtrip; extend
  test_model_switch_openai_api_mode with meta cases.
2026-08-17 12:58:51 -07:00
Teknium
d709d29f19 fix(agent): trim background_review to the enabled switch
Follow-up to #87400: drop the max_iterations and prompt_file knobs from
auxiliary.background_review. The aux model routing (provider/model/
base_url/...) predates #87400 and stays; the enabled switch and the
usage telemetry stay. The fork's iteration budget returns to the
historical hardcoded 16.
2026-08-16 10:27:52 -07:00
Ojas Sharma
7095e23eb2 fix(agent): attribute background-review usage and add cost controls
Persist fork token usage under session_model_usage task=background_review,
emit a per-fork completion log line, and expose enabled/max_iterations/
prompt_file so operators can see and bound the automatic review cost.

Address review feedback: load auxiliary.background_review once per spawn,
classify completion logs by summarize action prefixes, treat explicit
api_call_count=None as the documented default of 1, and WARNING on the
fail-open enabled-gate path.
2026-08-16 06:38:38 -07:00
LordMelkor
6efab28726 feat(auth): add key_cmd credential source for custom providers
Custom providers could only authenticate from a static credential (inline
api_key or a key_env env var). Enterprise gateways -- SSO/OIDC brokers, cloud
IAM, internal auth proxies -- issue short-lived bearers instead, so a value
copied into .env is stale within the hour: long sessions start returning 401s
and the user has to restart or run an external cron that rewrites .env.

The existing `secrets.command` source does not cover this: it runs once per
process at startup (subsequent calls are no-ops by design), so it cannot
re-mint a credential mid-session.

Add providers.<name>.key_cmd: a command that prints a token, wrapped at
resolution in a zero-argument callable. Both wire clients already accept a
callable api_key and invoke it per request (the Entra ID path established
this), so chat_completions, codex_responses and anthropic_messages all work
unchanged and always send a fresh credential. The callable also routes the
Anthropic client through its per-request Authorization hook, which is what
OAuth-gated gateway routes require -- so no per-vendor auth wiring is needed
anywhere in core.

- cached until shortly before the advertised expiry (60s leeway), so the
  helper runs about once per token lifetime rather than once per request
- expiry is read from the OAuth 2.0 relative `expires_in` when present, and
  otherwise from an absolute ISO 8601 deadline (`expiry`, `expiresOn`), which
  is what CLI token helpers commonly print. Reading only `expires_in` treated
  those helpers as advertising no TTL at all, cached their token for the life
  of the process, and returned 401 on every request once the real deadline
  passed. ISO parsing reuses hermes_cli.auth._parse_iso_timestamp rather than
  adding another datetime parser.
- no synthetic expiry: when no TTL is advertised, or the advertised one is
  unparseable or already past, the token is used and refreshed on 401 instead
  of re-minted on an invented schedule
- stdout contract matches OAuth 2.0 token endpoints and existing agent
  helpers (bare token or JSON access_token/expires_in); multi-line output is
  rejected rather than guessed at, so a misconfigured helper surfaces as a
  clear error instead of a corrupt-credential 401
- precedence: explicit --api-key still wins; otherwise key_cmd beats a
  static api_key/key_env on the same entry
- failures never include the helper's output (may hold a partial token) or
  the command string (may embed a client secret)

Resolution happens on two paths. agent/auxiliary_client.py resolves named
custom providers itself rather than calling _resolve_named_custom_runtime, so
key_cmd is honoured in both: wiring only the runtime resolver leaves the main
agent turn working while every auxiliary call (title generation, compression,
vision, embedding) falls back to the no-key-required placeholder and 401s.
Precedence is identical on both paths, so one config entry cannot yield two
different credentials depending on which resolver the caller reached.

Closes #84162

Signed-off-by: LordMelkor <kray@block.xyz>
2026-08-15 03:16:21 -07:00
Teknium
ce996d4057 feat(delegation): raise max_concurrent_children default 3 -> 10 (+migration) (#86745)
delegation.max_concurrent_children caps how many delegated children run in
parallel per batch (and concurrent background delegation units). The old default
of 3 needlessly serialized independent fan-outs (e.g. reviewing/​investigating N
PRs or issues at once), so large batches ran in slow chunks of 3.

Raise the shipped default to 10, which sits at/below the existing high-cost
advisory threshold (>10), so the default never trips the warning. Each child
still consumes API tokens independently, so this is a throughput/latency win the
user pays for in parallel token spend — the floor stays 1 and there is no
ceiling, so anyone can tune it down or up.

- config_defaults.py: default 3 -> 10; _config_version 36 -> 37.
- delegate_tool.py: _DEFAULT_MAX_CONCURRENT_CHILDREN 3 -> 10 (+ docstring).
- config_migrations.py: _migrate_to_37 lifts configs pinned at exactly the old
  default 3 to 10 (deliberate non-3 overrides preserved; unset inherits 10).
- cli-config.yaml.example: documented default updated.

Verified: default/fallback read 10, version 37, and the migration lifts 3->10,
preserves an explicit 5, and leaves unset untouched.

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-15 00:20:32 -07:00
joaomarcos
45bb486b26 fix(gateway): give in-flight cron work its own drain floor
`agent.restart_drain_timeout` defaults to 0 and governed every class of
in-flight work at once. That default is deliberate for chat turns: the
gateway announces the restart to the user and pre-marks the session
resume_pending, so interrupting one is cheap and recoverable.

A cron run has neither property. Nobody is waiting on it, it is written
to jobs.json as a permanent failure, and a recurring job simply skips to
its next schedule. Sharing the chat budget meant `_drain_active_agents()`
short-circuited on `timeout <= 0` before entering the wait loop, so the
drain reported `drain took 0.00s, timed_out=True, cron_at_start=1,
cron_now=1` — it detected the job and killed it anyway.

Cron work now drains on its own deadline, `agent.cron_drain_timeout`
(default 30s, 0 opts out). The floor is clamped to the shutdown-watchdog
leash minus a teardown reserve, so the longer wait can never consume the
post-drain cleanup window: being SIGKILLed mid-cleanup would leave the
job wedged at `last_status=running`, strictly worse than the bug. Being
bounded also means a cron-triggered restart cannot deadlock on itself.

The `timeout <= 0` special case is gone — an expired deadline expresses
the legacy "interrupt immediately" behaviour, so `timed_out` is always
computed from real state instead of asserted up front. The drain-timeout
warning now reports the elapsed wait rather than the configured budget,
which is what made "timed out after 0.0s" so confusing in the report.

Chat-only shutdowns are unchanged: `restart_drain_timeout: 0` still
interrupts chat turns immediately.

Relates to #82161 (complements #82195, which removes the `hermes update`
self-deadlock that triggered the reported instance).
2026-08-14 21:47:16 -07:00
Teknium
50d98fc1f3 feat(delegation): raise subagent iteration cap default 50 -> 250 (+migration) (#86506)
delegation.max_iterations is the per-subagent tool-call budget. The old
default of 50 truncated substantial delegated work: leaf agents spend
~15-20 turns on reconnaissance before producing output, then ran out of
budget mid-task and returned 'completed but unfinished' summaries. 250
gives real delegated work room to finish.

Changes:
- config_defaults.py: delegation.max_iterations 50 -> 250; _config_version 35 -> 36
- tools/delegate_tool.py: DEFAULT_MAX_ITERATIONS fallback 50 -> 250 (kept in
  sync with the shipped default to prevent drift)
- config_migrations.py: _migrate_to_36 lifts configs still pinned at exactly
  the OLD default 50 -> 250 on update, so existing installs inherit the new
  headroom. Any other explicit value (deliberate override) is preserved;
  unset inherits 250 at read time.
- cli-config.yaml.example: doc the new default

The cap is per-child and children run concurrently (max_concurrent_children
default 3), so this raises worst-case fan-out cost; delegation.child_timeout_seconds
(default 0 = off) remains available as a wall-clock guardrail, and users can
still pin a lower max_iterations explicitly.

Verified: migration lifts 50->250, preserves a deliberate 120, leaves unset
untouched (3/3); DEFAULT_CONFIG reads version=36, max_iterations=250, fallback=250.
2026-08-14 16:50:30 -07:00
kshitij
367f0c21ed feat(agent): resolve sequential tool deadline via timeouts.tools.sequential_call (#85125 2a)
Follow-up on the #84795 salvage: the sequential deadline gets its own
resolver key. Unset, it inherits the concurrent batch deadline (same
value, same HERMES_CONCURRENT_TOOL_TIMEOUT_S bridge) so the two executor
paths cannot drift by default; set, it can be tuned or disabled
independently. Documented in cli-config.yaml.example; 5 contract tests.

Deliberately NOT on run_bounded_sync: the executors extend deadlines
dynamically during human approval waits (authorization-gate excluded
seconds) — the shared primitive is fixed-deadline. Noted in the docstring.
2026-08-15 02:21:39 +05:30
kshitij
d6a5cb9725 Merge pull request #85147 from kshitijk4poor/feat/unified-deadline-layer
feat(agent): unified deadline layer — bounded execution primitive + timeout resolver (#85125 Phase 1)
2026-08-15 01:10:40 +05:30
Teknium
1c971769ec feat(gateway): concise background process notifications by default
Background process completions on messaging platforms now default to a
one-line status message (✅/❌ + command + duration; failures append a
short output tail) instead of dumping the raw output buffer into the
chat. New display.background_process_notifications mode 'concise' is
the default; 'all' keeps the old raw-dump behavior for anyone who wants
it. Config migration v35 moves users still on the old implicit default
'all' to 'concise' on their next update; explicit result/error/off
choices are preserved.
2026-08-14 00:26:11 -07:00
Simon van Laak
eb137762ff feat(slack): render tool progress as native plan/task cards (opt-in)
Adds platforms.slack.extra.native_task_cards: when enabled, live tool
calls render as Slack-native plan/task cards via chat.startStream /
chat.appendStream (task_display_mode: plan, task_update chunks) instead
of text/edit progress bubbles. ID-bearing tool_start/tool_complete
callbacks correlate concurrent same-name tool calls correctly; any
native API failure falls back to one continuously edited text update.
The stream is stopped exactly once when the turn finalizes.

Salvaged from PR #29496 onto current main (TurnRunner/TurnContext seam);
closes #29483.
2026-08-13 11:12:19 -07:00
kshitij
083f8a6071 feat(agent): unified deadline layer — bounded execution primitive + timeout resolver (#85125 Phase 1)
One shared foundation for the timeout/hang backlog instead of per-incident
site-local fixes:

- agent/deadline.py: run_bounded_async (thread-timer deadline that survives
  a blocked event loop, generalizing the telegram adapter primitive),
  run_bounded_sync, clamp_timeout (kills the #83220 time_t OverflowError
  class at the boundary), resolve_timeout (config.yaml timeouts: section >
  legacy env bridge > default), kill_process_tree (whole-tree termination
  for the #71148 orphan class), DeadlineExpired (our deadline, mechanically
  distinct from provider timeouts).
- tool_executor._resolve_concurrent_tool_timeout migrates onto the resolver;
  exact legacy env-var contract preserved (default 420, 0 disables).
- timeouts: accepted as a known config root; documented in
  cli-config.yaml.example.

Pure addition otherwise — no behavior change, no new env vars, no cache
impact. Later phases (#85125) migrate tool-execution, MCP, and subprocess
call sites onto these primitives.
2026-08-13 13:33:22 +05:30
Hans
52eb8eb533 feat(plugins): add pre_transcription hook and STT prompt threading
Adds a pre_transcription transform hook (prompt/language/model mutable,
file_path read-only, last-writer-wins per the transform_* convention)
fired before any STT backend, threads prompt to faster-whisper
(initial_prompt) and OpenAI/Groq/Mistral/DeepInfra (prompt), adds an
optional stt.prompt config key on the same plumbing, and keeps the
no-hook dispatch path byte-identical. Fixes #64168.

Documents the new surface for users: a "Transcription prompt
(vocabulary hints)" subsection in the configuration guide (composition
order, per-provider support matrix, length contract, privacy warning),
a pre_transcription entry in the hooks reference, and the mirrored row
in the plugins hook table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AG6LyYMvHC2o6HbVUozmVR
2026-08-12 19:01:30 -07:00
Teknium
de56e49a7c feat(plugins): add approval transport interface 2026-08-12 16:26:55 -07:00
Eva
acb7547dac fix(runtime): make nofile soft limit configurable 2026-08-10 17:02:56 -07:00
Jakub Wolniewicz
ae23b1f676 fix: complete kanban review lifecycle
Close the autonomous implement-review-rework loop, preserve parent gating and implementer provenance, distinguish downstream review cards, and surface legacy review dependency deadlocks immediately.

Co-authored-by: kaishi00 <6590895+kaishi00@users.noreply.github.com>
2026-08-10 12:43:46 -07:00
Teknium
21835cc951 docs(delegation): document frontier-planner / inexpensive-worker cost split
Surface the existing planner/worker cost-split capability as an explicit
strategy in the docs:

- delegation.md: new 'Cost strategy: frontier planner, inexpensive workers'
  subsection under Model Override, with a config.yaml snippet using the
  verified delegation.model / delegation.provider keys, the resolution order
  (base_url > provider > inherit parent; model applies in all cases, empty =
  inherit), and a note that delegate_task has no per-task model parameter —
  quality-sensitive tasks should use kanban's per-task override instead.
- kanban.md: matching 'Cost strategy: frontier orchestrator, inexpensive
  workers' subsection using the verified per-profile config mechanism
  (dispatcher injects profile-scoped HERMES_HOME at worker spawn) and the
  existing per-task model_override (--model/--provider, set-model, dashboard).
- zh-Hans mirrors for both pages.
- cli-config.yaml.example: cost tip comment under the delegation section.

Config resolution was live-verified against tools/delegate_tool.py
(_load_config + _resolve_delegation_credentials) with a temp HERMES_HOME:
delegation.model pins children to the sentinel model; with no delegation
keys, children inherit the parent model and credentials.
2026-08-10 10:56:04 -07:00
Teknium
244d296646 fix(personality): single-owner personality state + one-time reset migration
Personality persistence used to be split per surface: the TUI/desktop wrote
the NAME to display.personality while the CLI/gateway wrote rendered TEXT
into agent.system_prompt (and their /personality none only blanked the
text, leaving the name behind). When #81946 made display.personality
authoritative everywhere, stale names written long ago resurrected
personalities users had turned off - kawaii defaulting on after updating.

- hermes_cli/personality.py: new single owner of personality state.
  Built-in personality definitions, neutral-name normalization, rendering,
  availability (built-ins overlaid by agent.personalities), overlay
  resolution, and the ONLY sanctioned persistence path
  (persist_personality -> display.personality; never agent.system_prompt).
- v34 config migration: one-time reset of display.personality to none
  (announcing which personality was cleared and how to re-enable), plus a
  scrub of agent.system_prompt when it verbatim-equals a known personality
  render (machine-written by the old CLI/gateway). Hand-written manual
  prompts are never touched.
- All surfaces rewired through the module: CLI /personality (incl. active
  marker in the list), gateway /personality, TUI config.set + slash path
  (which previously applied without persisting), TUI config.get (reports
  the EFFECTIVE personality), completer, hermes config display, and the
  tui_gateway health probe.
- cli.py/config duplicates removed: built-ins now defined once; the
  desktop mirrors them from one lib module (src/lib/personalities.ts).
- Docs updated: selection lives in display.personality, built-ins always
  available, one-time reset note.
2026-08-09 10:33:58 -07:00
Teknium
5e1b50115f feat(compression): native OpenAI Responses server-side compaction for gpt-5.6
Opt-in via compression.codex_responses_native (default: false). When enabled,
gpt-5.6-family models on the direct OpenAI API (api.openai.com) or a ChatGPT
Codex subscription send context_management=[{type: compaction,
compact_threshold: N}] on Responses requests. OpenAI compacts server-side and
returns an encrypted compaction output item; Hermes captures it into the
existing codex_reasoning_items sidecar and replays it on later turns in place
of the pruned history — inheriting persistence, session replay, the
cross-issuer guard, and the encrypted-replay kill switch with zero new state.

Scope is deliberately hard-gated (agent/native_compaction.py, re-checked per
request): gpt-5.6 family only — gpt-5.1/5.2 fail server-side on the field
(HTTP 500 / stream stall, no structured rejection; live-verified) — and
direct OpenAI/Codex routes only; xAI, GitHub/Copilot, OpenRouter, relays,
and local servers never see the field.

Hermes' local compression stays armed as the fallback owner: the native
threshold is clamped ~8K tokens below the local trigger so the server
compacts first, and a structured provider rejection of context_management
disables native compaction for the session and retries without it
(one-shot guard in TurnRetryState).

Live-verified E2E on api.openai.com/gpt-5.6: server compaction fired at a
4K threshold, checkpoints captured and replayed, recall preserved across
3 turns; gpt-5.1 with the flag enabled stays clean (field never sent).

Direction credit: PR #76950 by @laryhorb explored native Responses
compaction; this is a minimal reimplementation on current main.
2026-08-08 11:24:45 -07:00
kshitij
e7667e56df docs(stt): honest memory-behavior wording for idle unload
The docs promised 'frees ~370MB RAM' — measured behavior on macOS/CPU
is that ctranslate2's allocator keeps the freed pages (RSS doesn't
visibly shrink); the concrete win is VRAM release on CUDA hosts and
process-internal reuse on CPU. Say exactly that instead.
2026-08-07 19:26:12 +05:30
kshitij
7b006ea6e8 feat(stt): idle unload for local whisper model
The local faster-whisper model singleton (_local_model) is loaded once
and never released — the 'base' model holds ~370 MB of RAM/VRAM for
the entire lifetime of the process, even when no voice messages arrive
for hours or days. On long-running gateway processes (especially with
local LLMs competing for the same GPU) this is wasteful.

Add a config-driven idle unload: after stt.local.unload_after_idle_seconds
(default 0 = never) of no transcription activity, a lightweight daemon
thread sets _local_model = None so the Python GC can reclaim the
ctranslate2 objects. The next voice message reloads the model
transparently (the existing lazy-load path handles it).

The watcher:
  - Checks every 30s whether idle time exceeds the configured threshold
  - Acquires _local_model_lock before unloading (prevents races with
    concurrent transcriptions that are mid-load)
  - Exits immediately if the model is already None (unloaded by another
    path, e.g. the CUDA fallback eviction)
  - Is restarted by each transcription with the current config value,
    so changing stt.local.unload_after_idle_seconds in config.yaml takes
    effect on the next voice message without a process restart

Default is 0 (never unload) — zero behavior change for existing users.
Recommended value for gateway processes: 300 (5 minutes).

15 tests: config resolution (garbage/negative/None fallbacks), unload
safety (already-None, lock acquisition), touch timestamp, watcher
lifecycle (unload after timeout, no unload within timeout, exits when
model already None, stopped on new start). Existing STT test suite
unchanged.
2026-08-07 19:26:12 +05:30
kshitij
5cff192b39 docs(stt): document the 12s short-clip gate for the cloud trim
The review-fold commit added the input-duration gate but the user-facing
docs and config example still implied every cloud clip gets trimmed.
One-line additions to both.
2026-08-07 19:26:04 +05:30
kshitij
a683ef95d2 feat(stt): pre-upload silence trim for cloud providers
Local faster-whisper gets Silero VAD (bf8004e3a) so silence never
reaches the model. Cloud providers got no such protection: the raw
file uploads untouched, so every second of silence in a voice note is
paid for twice — upload time and per-audio-minute billing — and cloud
Whisper hallucinates junk tokens on silent stretches exactly like
local Whisper did before the VAD hardening. A 13s voice note with two
long pauses is billed as 13s of audio to transcribe ~6s of speech.

Close the gap client-side: before uploading to a built-in cloud
provider (groq/openai/mistral/xai/elevenlabs/deepinfra), collapse long
pauses with ffmpeg's silenceremove filter, keeping
stt.cloud_trim_keep_ms (default 300) of every pause so word boundaries
and natural pacing survive. Uses ffmpeg, already a dependency of this
exact path via _transcode_audio_for_stt — no new dependency.

The trim is strictly best-effort — ALL of these upload the original
untouched, transcription never fails because of the trim:
  - stt.cloud_trim_silence: false
  - ffmpeg/ffprobe missing, trim failure, or timeout
  - trimmed result ~empty (mostly-silence clip: the provider, not a
    client-side dB heuristic, decides whether it contains speech)
  - trim saves <10% (re-encoding for nothing)

Command-type and plugin providers are deliberately NOT trimmed: they
may wrap local CLIs that want the original bytes or run their own VAD.

E2E (real ffmpeg + faster-whisper): 13.2s voice note with 7s pause ->
6.2s upload (-53%); transcript of trimmed audio matches the original
on both utterances. Dense-speech and all-silence WAVs correctly fall
back to the original. 22 unit+E2E tests; STT/voice suite failures
identical to upstream/main baseline (all pre-existing).
2026-08-07 19:26:04 +05:30
texasich
b3e9e91709 fix(gateway): configure turn lease timeout via yaml 2026-08-07 18:23:57 +05:30
Brooklyn Nicholson
dfc1cbebbd docs(config): document wake_word.capture in cli-config.yaml.example 2026-08-05 10:06:49 -06:00
konsisumer
23f8ae32c0 fix(agent): cap auxiliary LLM concurrency per task 2026-08-03 23:01:03 +05:30
Teknium
b2963f8034 docs: document agent.session_stall_timeout + compression timeout keys (re-review #7)
- website/docs/user-guide/configuration.md (en) and the zh-Hans
  translation gain a 'Session Stall Watchdog' section: default 300,
  0=disabled, notify-only semantics (never kills the turn — contrast
  gateway_timeout), one notification per stall episode, and the exact
  stall message text so it is greppable.
- cli-config.yaml.example: the two in-agent compression timeout keys
  (compression.context_timeout_seconds /
  compression.context_total_ceiling_seconds) are shown as commented
  lines next to session_stall_timeout's example for discoverability.
2026-08-02 16:16:36 -07:00
fangliquanflq
c2088efe9e feat(gateway): session activity watchdog, stall notify, compress timeout (#72424)
Three mechanisms to detect and notify when gateway sessions stall silently:

1. Mid-turn activity heartbeats stamped to SessionDB so hermes sessions list
   and hermes status show progress during long turns without new message rows.

2. Stall watchdog: when a busy session has pending inbound and the shared
   activity clock is idle past agent.session_stall_timeout (default 300),
   log a WARNING and notify the user once to try /new. Notify-only; does
   not kill the turn.

3. Compaction timeout: fenceless compress_context callers get a progress-aware
   host budget (compression.context_timeout_seconds default 120 idle,
   compression.context_total_ceiling_seconds default 600 ceiling). On timeout,
   cancel via commit fence, skip compaction without dropping messages, and
   continue the turn.

Closes #72016 (slices 1-3; slice 4 cumulative SSE stream-retry deadline
remains a follow-up).

Cherry-picked from PR #72424 by @fangliquanflq.
2026-08-02 16:16:36 -07:00
Zeraphim
0399711bec feat(desktop): add terminal font picker 2026-08-01 14:30:08 -07:00
Teknium
ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
Teknium
9d4bfd5e3d fix(config): register WAL sizing pragmas in DEFAULT_CONFIG
database.wal_autocheckpoint / database.journal_size_limit are now real
schema keys (default None = SQLite defaults) so the dashboard config
schema doesn't produce a single-field 'database' category, and the two
pragmas apply_database_pragmas reads are discoverable/documented.
2026-07-29 18:13:09 -07:00
Jasmine Naderi
91351b7b77 fix(state): make journal mode canonical and behaviorally verified
Use database.journal_mode as the sole non-secret operator setting, preserve the vulnerable-SQLite safety gate and existing WAL databases, validate explicit DELETE results, document the active config path, and cover real SQLite openers with behavioral tests.
2026-07-29 18:13:09 -07:00
Teknium
bf8004e3a8 fix(stt): kill faster-whisper silence hallucinations at the source
Local faster-whisper called model.transcribe with bare {'beam_size': 5}:
no VAD, cross-window conditioning on, no confidence filtering. Pure
silence produced hallucinated tokens (E2E: 5s anullsrc WAV -> 'You',
no_speech_prob=0.705) and noisy clips could produce runs of junk, often
in other languages.

Three-layer class fix, one shared owner for every local-whisper call
site (build_local_transcribe_kwargs):

1. Silero VAD filter (bundled with faster-whisper) on by default —
   silence never reaches the model. stt.local.vad: false restores the
   raw behavior for music/ambient transcription.
   stt.local.vad_min_silence_ms tunes chunk splitting (default 500).
2. condition_on_previous_text=False — one hallucinated token can no
   longer seed a self-reinforcing run; negligible cost for
   voice-note-length audio.
3. Segment confidence gate (_join_confident_segments): drop a segment
   only when no_speech_prob > 0.6 AND avg_logprob < -1.0 (openai-whisper's
   own heuristic shape; both must hit so quiet-but-real speech survives).
   Config: stt.local.no_speech_prob_threshold / logprob_threshold.

The WHISPER_HALLUCINATIONS blocklist in voice_mode.py stays as
last-resort defense but should now almost never fire.

E2E (real faster-whisper 'base', CPU int8):
  silence.wav  before 'You'                        -> after ''
  noise.wav    before ''                           -> after ''
  speech.wav   before/after 'Hello World, this is a test of the
               transcription system.' (unchanged)

Docs (EN + zh-Hans), DEFAULT_CONFIG, cli-config.yaml.example updated;
19 unit tests (kwargs contract, off-switch, confidence gate incl.
quiet-speech survival, _transcribe_local wiring), sabotage-verified.
2026-07-29 00:01:33 -07:00
Teknium
805c1c340c feat(stt): support OpenAI gpt-transcribe transcription model
Adds gpt-transcribe (OpenAI's new file-transcription model, $0.0045/min)
to the OpenAI STT provider:

- OPENAI_MODELS set: gpt-transcribe is recognized so provider
  auto-correction keeps it on OpenAI and rejects it on Groq
- Language hint wiring: gpt-transcribe replaces the singular
  'language' field with a 'languages' list; the API rejects the legacy
  field, so the hint is sent via extra_body {languages: [..]}
- Config comment (DEFAULT_CONFIG), cli-config.yaml.example, desktop
  settings enum, and docs (en + zh-Hans) updated
- Tests: model pass-through, languages-list hint shape, legacy singular
  hint preserved for gpt-4o-transcribe, Groq auto-correction

gpt-live-transcribe (realtime WebSocket, $0.017/min) is NOT wired here:
the file-based STT pipeline has no realtime session path; it belongs in
a future realtime/voice-mode integration.
2026-07-28 22:40:45 -07:00
Carlos Diosdado
ce4e2e6102 docs: add xAI TTS params to cli-config.yaml.example and tts.md 2026-07-28 11:55:01 -07:00
Alex Fournier
b1a5d67e71 Merge upstream main into fix/hermes-relay-anthropic-context
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-28 08:10:58 -07:00
Teknium
bc997a36a8 feat(stt): default global stt.language to 'en'
Whisper auto-detection frequently misidentifies short/accented clips,
which users experience as voice notes transcribed in the wrong language
(Teknium + CTO both hit this). The unified resolver from #73067 made a
global hint possible; this makes it the DEFAULT so stock installs stop
guessing. Non-English users set stt.language once; '' restores
auto-detect for multilingual use.

Deep-merge gives existing configs the new default automatically (no
_config_version bump needed); any explicit per-provider or global
language setting still wins.
2026-07-27 21:26:20 -07:00
Alex Fournier
14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Teknium
a10bd49ddd feat(stt): unify language resolution across all STT providers
Class-level fix for the 'STT transcribes the wrong language' issue family
(#55551, #50181 and siblings). Previously language handling was per-provider
chaos: local honoured stt.local.language, Groq/OpenAI/Mistral/DeepInfra sent
no language hint at all, xAI silently forced 'en', ElevenLabs used its own
language_code key, and there was no global setting.

- New _resolve_stt_language() helper: stt.<provider>.language >
  stt.language (new global key) > HERMES_LOCAL_STT_LANGUAGE > auto-detect.
- Threaded through ALL providers: local, local_command, groq, openai,
  mistral, xai, elevenlabs, deepinfra (shared OpenAI handler), command
  providers, and plugin dispatch.
- xAI no longer forces English when nothing is configured (auto-detect).
- Mistral Voxtral now receives a language hint when configured.
- stt.groq.model is now honoured from config (previously env-only).
- DEFAULT_CONFIG gains stt.language, stt.groq, stt.xai, stt.mistral.language.
- Tests: tests/tools/test_stt_language_resolution.py (11 tests, sabotage-
  verified) + full transcription suite green (236 passed).

Builds on cherry-picked contributor work from #19786 (@zombopanda),
#23161 (@materemias), #50684 (@BlackishGreen33).
2026-07-27 20:28:31 -07:00