Commit Graph

1335 Commits

Author SHA1 Message Date
kinsolee c750d5354a fix(sessions): prevent oversized transcripts from exhausting memory 2026-08-08 13:36:08 +05:30
teknium1 78bc9acdf1 chore(skills/document-to-action-items): promote to bundled tier
Fleet audit showed these task skills are commonly needed across users;
shipping bundled per Teknium's direction. Docs and tests follow the
bundled paths.
2026-08-07 10:35:42 -07:00
teknium1 7b8d0d800c chore(skills/document-to-action-items): tighten to hardline standards, move to optional
- description 214 -> 59 chars
- author credits Ben Barclay (benbarclay) first
- moved skills/productivity -> optional-skills/productivity (not a daily driver)
- dropped dangling 'linear' related_skills entry; prose points at approved destinations
- framed steps through Hermes tools (read_file, web_extract, xlsx, notion)
- trimmed template safety/verification boilerplate to doc-specific rules
- modern section order (When to Use / Procedure / Pitfalls / Verification)
- tests at tests/skills/test_document_to_action_items_skill.py (8 passing)
- docs regen scoped: per-skill page + one catalog row + one sidebar line
2026-08-07 10:35:42 -07:00
GodsBoy ca78c6d7a6 feat(plugins): load portable agent components 2026-08-07 09:44:21 -07:00
Teknium 9fad45fcda feat(kanban,mcp): orphaned-card reconciliation + per-server MCP identity header
Two small config-gated features:

1. Kanban orphaned-card reconciliation (kanban.reconcile_orphans, default
   true, config.yaml): a running card with broken claim bookkeeping
   (claim_lock or claim_expires NULL — crash mid-claim, manual SQL, DB
   restore) is invisible to all existing recovery paths
   (release_stale_claims requires claim_expires NOT NULL,
   detect_crashed_workers requires host-local lock + pid,
   detect_stale_running is config-disabled by default) and shows Running
   forever. New reconcile_orphaned_running() pass in kanban_db.py runs
   each dispatch_once tick: requeues orphans to ready with an explanatory
   comment, closes any leaked run, emits a 'reconciled' event, and defers
   when the recorded PID is still alive on this host (never requeue
   beside a live worker). Surfaced via DispatchResult.reconciled_orphans.

2. Per-server MCP identity header (mcp_servers.<name>.identity_header,
   config.yaml): optional {name, value_from: static|profile, value}
   mapping; the header is attached to that server's HTTP/SSE transport
   requests. 'static' sends the config value; 'profile' resolves the
   active Hermes profile name once at connect time (no per-call
   mutation). Explicit per-server headers of the same name (any casing)
   win. Invalid blocks warn-and-ignore; stdio servers warn-and-ignore.

Tests: tests/gateway/test_kanban_reconcile_orphans.py (9),
tests/tools/test_mcp_identity_header.py (13), all written first (RED)
then implemented (GREEN). No new HERMES_* env vars.

Inspired by: openai/symphony tracker reconciliation (Apache-2.0) +
Poke per-user MCP identity (idea-level).
2026-08-07 08:58:20 -07:00
Teknium 5396da844a docs: DX sweep — 7 verified-absent documentation items
- developer-guide/codebase-ownership.md (new): subsystem -> source dirs ->
  docs entry point map; complements the narrow CODEOWNERS proposal in #23751
  (docs table only, no .github/CODEOWNERS).
- contributing.md: document the .agents/checks/*.md repo-local review
  checklist convention (idea from goose, Apache-2.0).
- integrations/index.md: "Quick connect links" table with prefilled
  create-your-app deep links (Telegram BotFather, Discord
  ?new_application=true, Slack ?new_app=1, LINE, Feishu). Poke-inspired.
- guides/agent-email-address.md (new): dedicated agent mailbox via the
  bundled himalaya skill — setup, cron polling pattern, prompt-injection
  safety notes. Poke-inspired.
- user-guide/features/browser.md: Chrome 136+ silently refuses
  --remote-debugging-port on the default user-data-dir; dedicated profile is
  now mandatory (diagnosis from oh-my-pi, MIT).
- developer-guide/adding-providers.md: "Tool-call wire format" section
  linking the OpenAI chat-completions reference as the canonical shape for
  convert_messages/convert_tools.
- user-guide/features/tools.md: shell-init pitfall — heavy/interactive rc
  files (nvm, TTY-expecting blocks) break non-interactive agent terminal
  calls; interactive-guard pattern documented (from cline, Apache-2.0).

Both new pages registered in sidebars.ts. Validated with npx docusaurus
build (en + zh-Hans green; zh-Hans relative-link warnings are the known
pre-existing untranslated-page noise).
2026-08-07 08:58:08 -07:00
Teknium ed903f953e feat(cron): pre-dispatch configuration validation (blocked_config + alert-once)
Validate a job's configuration BEFORE any agent machinery is constructed:

- missing provider API key (AuthError from a read-only
  resolve_runtime_provider probe; skipped when a fallback_providers chain
  is configured, since auth-fallback may rescue the run)
- attached skill not ready (skill_view readiness_status=setup_needed —
  missing required env vars / commands / credential files)
- delivery platform unknown or unconnected (deliver=local/origin/all are
  never checked; gateway-config load failures fail open)

On a failing check run_job returns a [blocked_config]-marked error without
constructing AIAgent/MCP/etc, so a misconfigured job never burns an LLM
call. run_one_job records last_status='blocked_config' and delivers the
alert exactly ONCE across ticks (persisted preflight_alerted bit — the
alert-once shape from the #73506 dead-pin auto-pause); the next healthy
run clears the marker so a future break re-alerts. Every preflight check
fails open: only an affirmative misconfiguration verdict blocks.

Config: cron.preflight (default true); `cron.preflight: false` restores
the old fail-during-run behavior. Documented in the cron user guide and
config defaults.

mark_job_run gains an optional status= override (unblocked call shape
unchanged) and drops preflight_alerted on any successful run.

Tests: tests/cron/test_preflight_config.py (blocked_config + no agent +
single alert across two ticks, healthy job unaffected, recovery clears
dedup, fallback-chain rescue, opt-out restores old behavior, skill
readiness miss, unknown delivery platform, deliver=local never loads
gateway config). Full tests/cron/ + cronjob tool suite green (525 tests).

Ported from: paperclipai/paperclip execution-semantics §5 (MIT);
in-repo precedent: #27948, #73506
2026-08-07 08:57:53 -07:00
HexLab98 2d0c2682c7 docs: document the gateway agent cache memory budget
Record why the cache needs a third bound and what the pressure pass will and
will not shed, so an operator tuning agent.agent_cache knows which knob to
reach for. Adds the config keys to the session-lifecycle appendix and a user
guide section covering the "auto" cgroup-derived budget.
2026-08-07 21:02:33 +05:30
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
HexLab98 d7fb503c27 docs: note that the non-stream stale budget covers cron and subagents
The timeout table already lists the stale non-stream detector, but the
prose read as if it only guarded the interactive path. Spell out that it
bounds the inline cron / delegated-subagent calls too, and name the
accepted-then-silent failure mode it recovers from.
2026-08-07 15:32:37 +05:30
kshitij f5784617e8 refactor: fold simplify-code review findings
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps,
  ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior
  check_*_requirements combined credential env checks with the install,
  so a platform configured via PlatformConfig.extra (which is_connected
  accepts) would pass enablement, reach create_adapter(), and have the
  'installer' veto on env-var grounds before installing anything —
  re-creating the #79812 deadlock for extra-configured setups.  The
  combined deps+credentials functions remain for setup/status callers.
- matrix/feishu passive probes: use the existing lazy_deps.is_available()
  instead of hand-rolling 'not feature_missing(...)' (reuse finding).
- teams: module docstring no longer recommends bare system pip (the
  PEP 668 trap purged everywhere else); docs troubleshooting row updated
  to match the new hint text.
- wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE'
  (ensure_and_bind mutates the module dict directly; nothing assigns).
- tests: parametrized wiring contract for all 8 lazy-installable
  platforms — ensure_deps_fn present and distinct from check_fn
  (behavior contract, not identity snapshot, so renames don't churn it).
2026-08-07 13:28:43 +05:30
kshitij a658dfe509 fix: address self-review findings on the check_fn/ensure_deps_fn split
- gateway/config.py: rewrite the stale enablement-pass header comment that
  still described check_fn as 'the single source of truth for are-my-env-
  vars-set' / 'lazy-installs it' — both false under the new contract.
- teams: check_requirements docstring wrongly claimed credential checks
  (body checks only SDK/aiohttp presence); derive install_hint from the
  canonical LAZY_DEPS pins + sys.executable instead of hardcoding
  '~/.hermes/hermes-agent/venv/bin/pip' and version pins (wrong under
  HERMES_HOME overrides / profile installs; pins go stale on CVE bumps);
  connect() fatal-error hints now point at the venv pip instead of bare
  system pip (the PEP 668 trap the docs warn about).
- teams docs: drop exact version pins from the two manual-install commands
  (LAZY_DEPS is the source of truth; unpinned installs still work and the
  text can't go stale).
- hermes_cli/status.py: per-entry exception guard around check_fn so one
  raising probe can't abort the listing of all remaining plugin platforms
  (aligns with the other three call sites).
- tests: rename test_register_check_fn_is_active_lazy_installer ->
  test_register_splits_passive_probe_from_active_installer (name said the
  opposite of what it verifies).
2026-08-07 13:28:43 +05:30
xxxigm 042c309ec5 docs(teams): native gateway start and Hermes-venv dependency install
Step 5 only showed docker compose from a clone; native/systemd users
hit missing compose files and PEP 668 system-pip failures.
2026-08-07 13:28:43 +05:30
Teknium 71dc211b9e docs(cron): document async manual runs and per-run prompt context
Covers the behavior shipped in #80807 (background dispatch for
cronjob action='run') and #80838 (per-run '## Run Context' prompt,
gateway-loop delivery): immediate return with handle, completion
re-entering the conversation, in-flight dedupe, transient context
injection with prompt scanning, and the sync fallbacks.
2026-08-06 23:22:42 -07:00
Teknium 32e7fb07a0 feat(/learn): expansive knowledge-base skills for books and large corpora
Inspired by virgiliojr94/book-to-skill (MIT): /learn now picks the skill
shape by the source. Workflows and small sources still get one tight
SKILL.md; books, paper stacks, specs, and large doc corpora get a
knowledge-base layout — a lean always-loaded SKILL.md index plus one
distilled file per chapter/topic under references/, loaded on demand via
skill_view so query cost stays proportional to the answer.

- agent/learn_prompt.py: new _KNOWLEDGE_SKILL_STANDARDS block (index +
  per-chapter references/, structure-not-summary distillation, never
  reproduce source passages, fold-in instead of duplicating) and a
  _SOURCE_HYGIENE block pinning extracted source text as data and
  dropping invisible/bidi Unicode (Trojan Source class). Clarified that
  the ~200-line cap and hub-skill ban apply to SKILL.md itself, not a
  knowledge skill's own references/ files.
- tests: contracts for the knowledge-base layout, the three embedded
  standards blocks, and the source-hygiene coverage.
- docs: skills.md documents the knowledge-base shape.
2026-08-06 22:14:52 -07:00
teknium1 eb1e63090a fix(skills): align hermes-agent-skill-authoring with hardline authoring standards
The in-repo skill-authoring skill taught the validator's ceilings (1024-char
descriptions, 'Use when ...' phrasing) instead of the repo's review standards,
so agents following it produced skills that fail review: 240+ char
descriptions, author 'Hermes Agent' with no human credit, no bundled-vs-
optional decision, dangling related_skills, no platforms audit, no tests, no
docs regen, and machine-local /home/bb/... paths baked into prose.

Rewritten to teach the hardline standards from AGENTS.md:
- description <= 60 chars, one sentence, ends with period
- author credits the human contributor first
- bundled vs optional tier decision (5+ sessions/month bar; default optional)
- no router/index/hub skills
- platforms: audited against actual scripts, POSIX-signal table
- related_skills must resolve in-repo
- Hermes-tool framing instead of raw shell prose
- tests at tests/skills/ + docs regen with scope discipline
- removed machine-local paths; validator limits marked as NOT the standard
2026-08-06 22:07:51 -07:00
teknium1 0957277f2f refactor(skills): move polymarket to optional-skills/finance
Per the 'when in doubt, optional' rule — niche prediction-market data
skill that sees no regular use; belongs alongside stocks in the finance
optional category rather than the default bundle.

Install via: hermes skills install official/finance/polymarket
2026-08-06 11:30:58 -07:00
SmokeDev a94ebf5f5e fix(delegation): harden steer lifecycle ownership 2026-08-06 09:40:27 -07:00
SmokeDev 60e1f7517c fix(delegation): surface a child's undelivered steer instead of dropping it
The turn finalizer already hands back steer text that queued after the
final tool batch — result["pending_steer"], with the comment "hand it
back to the caller so it can be delivered as the next user turn instead
of being silently lost." Every interactive surface honors that contract
(cli.py, gateway/run.py, tui_gateway/server.py all requeue it). The
delegation layer doesn't: _run_single_child never reads it, so a steer
queued into a delegated child that finishes first vanishes with no trace
in the completion entry. There is also no sanctioned sender: the registry
has interrupt_subagent() but no redirection-side mirror, and session.steer
cannot reach children (lazy watch sessions have agent=None, so it 4010s).

Complete the contract for delegated children — both halves:

- steer_subagent(subagent_id, text): redirection-side mirror of
  interrupt_subagent(). Resolves the live child in _active_subagents and
  queues text via AIAgent.steer(). True means queued, not delivered.
- missed_steer retention: when the child's result carries pending_steer,
  _run_single_child names it on the completion entry (missed_steer field
  plus a summary note) so the parent can re-issue the guidance instead of
  trusting it landed. This is what makes adding a sender safe: without it
  the finish-before-drain race silently loses the text — the exact loss
  the finalizer contract exists to prevent.
- subagent.steer gateway RPC beside subagent.interrupt so programmatic
  hosts (dashboard, voice layers, ACP bridges) get an in-tree caller;
  catalogued in programmatic-integration.md.
- docs: "Steering a Running Subagent" section in delegation.md covering
  the queued-vs-delivered semantics.

Tests: registry-level steer coverage (delivery, unknown id, empty text,
dead record, raising agent), the finish-before-drain race retaining
missed_steer, and the RPC contract (4000/4002 validation, queued and
rejected envelopes).
2026-08-06 09:40:27 -07:00
kshitij aaf9688519 refactor(gateway): extract the hygiene recovery gate and forward the failure reason
Follow-up to c0d974b19 (#79741). Three review findings against that commit,
none of which change the escalation behaviour it shipped.

1. The recovery decision lived inline in `_handle_message_with_agent`, a
   ~2000-line async method, so the only way to pin it was a test that read
   `inspect.getsource(...)` and asserted on substrings. AGENTS.md bans reading
   source in tests outright and names this exact situation: "if the logic lives
   inline in a god-file (gateway/run.py) and extracting it feels disruptive:
   that's the actual signal to do the extraction, not to regex around it."

   Those tests were not merely stylistically wrong, they were actively harmful.
   One asserted the substring `_new_tokens < _approx_tokens` was PRESENT -- so
   it passed while the gate had the bug that substring represents, and had to be
   edited when the gate was fixed. It failed on correct code and passed on
   broken code, in one assertion.

   Extracted `hygiene_compaction_recovered()` as a module-level pure predicate
   and replaced the three source-reading tests with eight direct unit tests.
   The extraction immediately earned itself: the new tests caught a `NameError`
   (the predicate called `compression_made_progress` while the module bound it
   under an alias) that a source-text assertion cannot see, because the symbol
   is spelled correctly in the source and only fails at runtime.

2. The gate inferred "did the transcript actually get rewritten" from a numeric
   side effect -- the degenerate "did not rotate or compact in place" path
   (#21301) reuses the pre-compression counts -- when the booleans
   `_hyg_rotated` / `_hyg_in_place` were already in scope and explicitly set
   False on that path. The predicate now takes them directly, so a future edit
   that re-estimates instead of reusing the old counts cannot silently defeat
   the escalation.

3. `_record_hygiene_cooldown` passed no `error` to
   `record_compression_failure_cooldown`, which writes `compression_failure_error`
   unconditionally -- so a hygiene failure clobbered to NULL whatever reason the
   in-conversation path had recorded, and readers then show the user "unknown
   error" (agent/manual_compression_feedback.py, gateway/slash_commands.py). The
   reason was already in hand at both call sites. Pre-existing from #74136 but
   amplified by escalation: a blank reason on a 45-minute cooldown is far more
   user-visible than on a 5-minute one.

Also: the ladder docstring described the compressor's absolute 60/300/900s
ladder while the constant is multipliers (1, 3, 9); the config docs still
described `hygiene_failure_cooldown_seconds` as a flat interval rather than the
first rung of a capped ladder; and `PersistentState.hygiene_failure_streak` now
documents that it is process-local by design -- keying on `session_key` is what
survives compaction rotation, which the persisted `compression_*_streak`
columns cannot express since they key on the rotating `session_id`. Making it
durable is a schema change, tracked on #79624 rather than smuggled in here.

Also replaces the file's hand-written `_Runner` stub with
`object.__new__(GatewayRunner)` (already the idiom elsewhere in the same file).
The stub reimplemented `_session_state` and `_peek_session_state`, so the tests
exercised copies that could drift from production; using the real class
immediately made one assertion stronger -- on a fresh runner `_sessions` does not
exist at all until something materialises it, so the reset provably did not even
create the map.

A review pass on this follow-up then caught that the CALL SITE was still
unbound: deleting the whole `if not _hyg_aborted: if
hygiene_compaction_recovered(...)` block left every ladder test green, because
the unit tests prove the predicate correct without proving it is wired in. The
merged commit had the same gap and its only cover was the banned source-reading
test. `test_session_hygiene_forces_in_place_compaction_with_bound_session_db`
now spies the reset on a genuine in-place compaction, so deleting the wiring
fails. Two earlier attempts at this test did NOT close the gap -- asserting on
streak VALUES passes either way, since the streak is 0 whether or not the gate
ran; only a positive spy assertion on a recovering run detects the deletion.

Same pass also corrected an overstatement: point (2) is hardening, not a live
bug. The degenerate path also sets `_new_count = _msg_count` and `_new_tokens =
_approx_tokens`, and `compression_made_progress(n, n, t, t)` is always False, so
the merged code already declined to reset there. A 200k-trial fuzz over the
reachable state space found zero behavioural disagreements between the merged
gate and this one. The guard's value is surviving a future edit that stops
reusing those counts.

Tests: 28 in tests/gateway/test_hygiene_failure_cooldown_ladder.py (8 new unit
tests for the predicate, 3 for reason forwarding, 3 source-reading tests
deleted). All 5 mutations caught -- including one that restores the hand-rolled
comparison and one that removes the rotated/in_place guard. Two mutations
initially SURVIVED and exposed vacuous tests of my own: the no-rewrite test used
counts the progress predicate already rejects, so it passed without binding the
guard at all; it now passes counts that read as progress on their own, proving
the guard is what rejects them. gateway hygiene + session-state + agent
compression-progress suites: 54 passed; ruff clean.

Refs #79624
2026-08-06 15:04:53 +05:30
Teknium 6518aa184e feat: /heartbeat — recurring session re-entry prompt fired when idle
/heartbeat every <interval> <prompt> gives the current session one
recurring instruction. When the session is idle and the interval has
elapsed, the prompt is injected as a plain user turn — same
conversation, same context, prompt cache and role alternation
untouched.

- CLI: idle-poll watchdog thread (wake-word watchdog pattern) feeding
  _pending_input; gateway: single gateway-wide async poller injecting
  through the adapter FIFO. Busy sessions coalesce their tick to the
  next idle poll.
- Missed ticks coalesce (anchor resets on fire) — a busy hour yields
  ONE heartbeat turn, never a backlog. Real user messages always win.
- 60s interval floor; injected prompt carries a don't-invent-work
  guard so idle heartbeats don't generate busywork.
- State persists in SessionDB.state_meta (heartbeat:<session_id>),
  survives /resume, migrates across compression session rotations
  alongside /goal state.
- Session-scoped and in-process by design — durable cross-process
  schedules remain the cron subsystem's job (docs draw the boundary).
- Slack stays under the 50-slash cap via /hermes heartbeat; ghost-text
  suggester now prefers the shortest prefix match so /he still
  suggests /help.

Adapted from the session-heartbeat concept in Prime Intellect's
Prime-Agent (/heartbeat).
2026-08-05 22:32:55 -07:00
Teknium 6e041d5244 feat(goals): quality gates — deterministic commands that must pass before /goal completes
/goal gate add <command> attaches shell commands to the active goal.
Gates run at turn boundary BEFORE the LLM judge: a failing gate skips
the judge entirely and feeds its exit code + bounded output tail back
as the continuation prompt, so the agent iterates against concrete
evidence instead of a prose verdict.

- Unchanged-workspace skip: a gate that failed on an identical
  workspace (git HEAD + status fingerprint) is not re-run — the
  recorded failure replays and the attempt count advances.
- Bounded retries (default 3) + per-gate timeout (default 300s);
  exhaustion auto-pauses the goal like the turn budget does.
- Gates persist in SessionDB.state_meta with the goal (survive
  /resume and compression rotation); pre-gate goal rows load
  unchanged.
- /goal gate [list|add|remove|clear] on CLI + gateway; 'gate' added
  to the mid-run control-verb whitelist (gates only run at turn
  boundary, so editing the list mid-run is safe).

Adapted from the quality-gate concept in Prime Intellect's Prime-Agent
(--autonomous-gate).
2026-08-05 22:32:39 -07:00
brooklyn! 7e16241825
feat(wake): hands-free wake word for remote desktop via client mic streaming (#79491)
* feat(wake): client-capture wake word for remote desktop

Remote headless backends have no PortAudio mic, so "hey hermes" fails even
when openWakeWord is installed. Let the desktop stream 16 kHz int16 PCM via
wake.feed while detection stays server-side.

- wake_word.capture: auto|local|client (+ GUI client_capture prefer)
- WakeWordDetector external_audio queue + feed_audio API
- wake.feed RPC; wake.start/status report capture + frame_length
- Desktop getUserMedia feeder; stop on wake.detected, re-arm after voice
- Docs + unit tests (26 pass in tests/tools/test_wake_word.py)

* fix(wake): address review on client-capture re-arm and feed queue

- wake.status reports effective capture from the armed detector (client vs
  local), plus frame_length/sample_rate; GUI status probes prefer client
- Gateway test doubles accept external_audio on start_listening
- Desktop PCM feeder uses a bounded ordered queue instead of dropping frames
  while a wake.feed RPC is in flight
- /wake on and status/re-arm paths pass client_capture so remote reattach works

* fix(wake): auto capture keeps the backend mic when one exists

With capture:auto the desktop always preferred client streaming, so a local
desktop with a working backend mic silently switched from PortAudio to
getUserMedia default-device — dropping wake_word.input_device selection
(#74363). A ready backend input now wins under auto; client capture is the
fallback for a preferring surface on a mic-less backend, and capture:client
still forces streaming.

Also removes the dead auto branch (both arms returned local) and lets the
client-feed test skip cleanly when numpy is absent.

* perf(desktop): coalesce wake.feed frames

Sending one 80 ms frame per RPC is ~12.5 gateway calls/s for as long as the
ear is armed. Drain up to 4 queued frames into a single wake.feed payload
(backend feed() already splits long buffers into engine frames) — ~3 RPCs/s
steady-state. Fix the wake.feed size-cap comment (64000 bytes = 2 s, not
0.5 s).

* docs(config): document wake_word.capture in cli-config.yaml.example

---------

Co-authored-by: Andrew <drew@kainotomic.com>
2026-08-05 10:32:38 -06:00
Andrew 105fbf6b7d feat(wake): client-capture wake word for remote desktop
Remote headless backends have no PortAudio mic, so "hey hermes" fails even
when openWakeWord is installed. Let the desktop stream 16 kHz int16 PCM via
wake.feed while detection stays server-side.

- wake_word.capture: auto|local|client (+ GUI client_capture prefer)
- WakeWordDetector external_audio queue + feed_audio API
- wake.feed RPC; wake.start/status report capture + frame_length
- Desktop getUserMedia feeder; stop on wake.detected, re-arm after voice
- Docs + unit tests (26 pass in tests/tools/test_wake_word.py)
2026-08-05 10:03:48 -06:00
kshitij cc245e84d2 fix: correct cron mid-run restart claim in salvaged docs
The original PR #78453 said 'A job that was mid-run during a restart
resumes according to the attempt policy described in this page.' This
is misleading — the existing docs explicitly state 'Unknown attempts
are audit records and are never automatically rerun.' Corrected to
accurately describe: the mid-run attempt is marked unknown (not retried),
but the job's next scheduled tick fires normally.
2026-08-05 21:33:44 +05:30
witcheer 2183ed3921 docs: fix stale PATH location in windows-native common pitfalls 2026-08-05 21:33:44 +05:30
witcheer f8aed15cb1 docs: warn against pointing two agents at one Hermes home (memory, profiles, FAQ) 2026-08-05 21:33:44 +05:30
witcheer e20cfd35e0 docs: four small accuracy fixes
- cron: state explicitly that job definitions survive updates, gateway
  restarts and reboots (asked directly in #37542)
- mcp: add a Claude Code bridge tip - mcpServers maps to mcp_servers and
  hermes import-agent migrates it (the MCP page never says 'mcpServers'
  in the client direction; arrivals from Claude Code get no pointer)
- installation: surface loginctl enable-linger in the non-sudo/service
  user section where affected users start (currently only on the
  gateway page; #43748)
- sessions: document optimize, optimize-storage, repair, recover and
  retitle-skills in the CLI reference (shipped in v0.19.1 --help but
  absent from the table) and recommend non-destructive optimize before
  prune in the db-growth tip

All wording verified against hermes v0.19.1 --help output and the live
pages on 2026-08-04.
2026-08-05 21:33:44 +05:30
witcheer abaa43ed7c docs: add 'Which File Does What?' - one-page map of SOUL/USER/MEMORY/AGENTS
The four-file map is currently split across the memory, personality and
context-files pages; 'which file is my agent's brain' is one of the most
frequent support questions (e.g. #20245, #29476). One master table, the
frozen-snapshot rule surfaced with a link, and the two canonical mix-ups
answered directly. Content is drawn from the existing three pages.
2026-08-05 21:33:44 +05:30
witcheer b4312f92c6 docs: state the /goal vs Kanban boundary on both pages
The goals page never mentions Kanban and the kanban page references /goal
only inside the goal-mode-cards section, so users assume /goal hands work
to the board (see #26116 - /goal is single-session continuation only).
Adds a decision section to goals.md and the inverse note to kanban.md.
2026-08-05 21:33:44 +05:30
xxxigm 62800ddadb docs(delegation): note in-flight model waits count as progress
Clarify that activity-timestamp ticks during a provider wait keep the
staleness monitor from treating a slow completion as a wedged child.
2026-08-05 14:00:25 +05:30
Teknium a991dfc25d docs: document /personality none|default|neutral reset across personality docs
The reset keywords have existed in both CLI and gateway handlers since
June but were undocumented — users couldn't find how to cancel a
personality overlay. Adds a 'Resetting to the default' section to the
personality feature page and mentions the reset in the CLI guide,
slash-command reference (both tables), and messaging command table.
2026-08-03 12:29:49 -07:00
konsisumer 23f8ae32c0 fix(agent): cap auxiliary LLM concurrency per task 2026-08-03 23:01:03 +05:30
EndeavorYen 952d86b797 fix(file-sync): serialize concurrent sync cycles 2026-08-03 22:53:32 +05:30
Carbon 48e12a06fa perf(tools): shrink lazy tool catalog overhead 2026-08-03 19:11:30 +05:30
WojtekMR3 6611d87003 feat: reset-aware primary restore — stay on fallback until the rate-limit window resets
restore_primary_runtime retries the primary every turn once the 60s
transient cooldown clears. For subscription-window limits (Claude
Pro/Max 5h windows, Codex weekly caps) the reset is hours or days away,
so every retry is a guaranteed failure costing two provider switches
and two prompt-cache invalidations per turn.

Add CredentialPool.next_available_at() (earliest reset across exhausted
entries; None when available now or no reset info) and gate the restore
on it: skip while the primary's pool says nobody can serve, restore on
the first turn after the reset elapses. Fail-open: any gate error or
missing reset info falls through to the existing per-turn retry, so
recovery can never be later than today. Cross-provider fallbacks
consult the PRIMARY's pool (not the attached fallback pool), reusing
the loaded pool for the existing rebind to keep auth reads at one per
restore.
2026-08-03 19:02:12 +05:30
Kyzcreig ad345a99d8 feat(gateway): add opt-in 'latency' runtime footer field
The runtime footer (`/footer`) shows what model ran and how full the context
is, but not how long the turn took. On a messaging platform there is no
progress bar and no shell timer — a turn that took 4 seconds and one that took
four minutes produce visually identical replies. Users comparing models,
providers, or reasoning levels have no at-a-glance signal for the one
dimension they most often care about, and "was that slow or did I imagine it?"
is unanswerable after the fact.

Adds a `latency` field to the existing footer machinery, rendering the
wall-clock duration of the agent run: `<1s`, `22s`, `1m05s`.

`gateway/run.py` measures with `time.monotonic()` immediately around the
`self._run_agent(...)` await in `_handle_message_with_agent` — the same
function that already builds the footer, so the value is the user-perceived
turn duration (monotonic, so it is immune to wall-clock/NTP adjustment).

`latency` is deliberately NOT in `_DEFAULT_FIELDS`. It is opt-in via
`display.runtime_footer.fields`. Every existing footer — and every footer a
user has today without touching config — renders byte-identically.

This is enforced by tests, not just asserted:

- `test_latency_not_in_default_fields` pins the default tuple.
- `test_resolve_footer_config_default_fields_exclude_latency` pins what
  config resolution produces for an untouched config.
- `test_default_footer_renders_byte_identically` pins five exact output
  strings for default-config renders **while supplying `turn_seconds`** —
  proving that even when the caller measures timing, a default-configured
  footer does not show it.
- `test_default_build_footer_line_ignores_turn_seconds` asserts
  `build_footer_line(...) == build_footer_line(..., turn_seconds=125.0)`
  under default fields.

Adding `latency` to `_DEFAULT_FIELDS` fails 11 of these tests.

No new config surface (reuses `display.runtime_footer.fields`), no new env
vars, no new core tool, no new model-facing schema. One new module-private
helper (`_format_latency`), one new keyword argument threaded through the two
existing footer functions, and 3 lines in `gateway/run.py`.

`turn_seconds` defaults to `None` and the field is skipped when it is `None`
or negative, so any call site that does not measure timing keeps working
unchanged.

`tests/gateway/test_runtime_footer.py` (+185): `_format_latency` boundary
table (sub-second, rounding at 59.4/59.6, the `m{:02d}s` zero-pad, 60m), the
render/skip/opt-in matrix, field-order placement, `build_footer_line`
threading, and the byte-stability block above.

RED-proved by mutation — each of these breaks tests:
- `latency` added to `_DEFAULT_FIELDS` → 11 failures
- dropping the `turn_seconds is not None and >= 0` guard → 2 failures
- `{sec:02d}` → `{sec}` → 6 failures
- `build_footer_line` not threading `turn_seconds` → 1 failure

51 passed in `tests/gateway/test_runtime_footer.py`; 54 passed across the
footer blast radius. `ruff check` clean.
2026-08-03 17:16:57 +05:30
Teknium a6defd4f15 fix(skills): match evidence quotes through markdown markup
Live-run findings from a real fact-checking task (ankylosing spondylitis
genetics, 7 authoritative sources) against the new mode:

- Verbatim check rejected a legitimate quote because web_extract returns
  markdown: the MedlinePlus sentence is "including _[ERAP1](https://...)_,
  _[IL1A](...)_" on the wire but plain prose to a reader. The agent was
  forced onto a weaker evidence fragment — the opposite of the point.
  Matching now canonicalizes inline links to their label and drops
  emphasis/code markers and backslash escapes on both sides, so quoting
  the sentence a reader sees works. Paraphrases are still rejected.
- Escaped asterisks (HLA-B\*27) no longer have to be reproduced in the
  quote, so extractor artifacts stop leaking into rendered evidence.
- New `render --replace-in <draft>`: rewrites a draft's Sources block in
  place, idempotently. Previously the only path was hand-slicing the
  file, which also tripped over the emitted heading being `## Sources`
  while the prose said "Sources:".
- verify stats: report the provenance total that the percentage is
  actually computed from (cited + [unverified], counted once), and print
  the line as `info:` instead of `warn:` when nothing is wrong. The old
  line printed 17 cited / 2 unverified next to 72%, which does not
  reconcile — a sentence can be both.
- SKILL.md documents the emitted heading, --replace-in, and exactly what
  counts as a prose sentence for --min-coverage.

7 new tests (47 total) using the real MedlinePlus/Frontiers markup;
6 sabotage runs, all red.
2026-08-02 16:18:28 -07:00
Teknium 4660673a3c feat(skills): add fact-checking mode to grounded-citations
Extends the citation ledger with evidence-backed fact-checking:

- New `quote` subcommand attaches verbatim supporting quotes to a
  source; the quote is rejected unless it appears verbatim
  (whitespace/case-insensitive) in the fetched page text, so a
  paraphrase or misremembered figure cannot masquerade as evidence.
- `verify --evidence` fails a draft whose cited sources carry no
  attached quote.
- `render --style evidence` prints each source's quotes beneath its
  URL, showing the claim -> source -> exact-text chain.
- `[unverified]` marker declares model-knowledge claims; counts toward
  --min-coverage so provenance is declared for every sentence without
  forcing fake citations.
- SKILL.md: new Fact-Checking Mode section + pitfalls; version 1.1.0.
- 10 new tests (40 total), all proven live by sabotage runs.

Covers the fact-checking/evidence-transparency half of #28289.
2026-08-02 16:18:28 -07:00
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
Teknium 2fb0aa1c0e fix(agent): enforce bounded, surfaced commit-phase waits past context_total_ceiling_seconds
The post-begin_commit() waiter previously called unbounded future.result(),
so the advertised compression.context_total_ceiling_seconds was silently
unenforced for commit-phase hangs. The commit still must complete (abandoning
an in-flight SessionDB mutation would diverge live messages from durable
state), but the wait is now bounded in increments against the remaining
ceiling: on ceiling breach the overrun is logged (WARNING escalating to
ERROR), surfaced once through the user-visible warning channel via the new
on_commit_overrun callback (wired to _emit_warning in run_agent.py), and the
host keeps waiting in bounded slices until the commit finishes.

Documented guarantee (config comment + docs, en/zh): summary phase bounded
by the ceiling; commit phase logged + surfaced if it exceeds it — never
silently hung, never abandoned mid-commit.

Test updated to assert the surfacing fires (previously accepted a silent
over-ceiling wait); adds coverage that a raising overrun callback cannot
break the commit wait.
2026-08-02 16:16:36 -07:00
fangliquanflq 06c7f9b26f fix(agent): clarify compress_context ceiling is pre-commit only
Once begin_commit() wins, SessionDB mutation cannot be fence-cancelled;
document that context_total_ceiling_seconds covers the summary phase only
and pin the hang-wait contract in tests.
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
Teknium 81c7e5de48 docs(a2a): website docs page + canonical agent-card.json path in prose
- New website/docs/user-guide/messaging/a2a.md: when/where to use A2A
  (cross-machine, specialist peers, being callable) vs delegation/kanban
  for same-machine multi-agent; enable, outbound tools, inbound surface,
  security model, env reference, quick test, troubleshooting. Registered
  in sidebars.ts and the messaging index.
- README/DESIGN/plugin.yaml/protocol.py prose updated to name the A2A
  v1.0 canonical discovery path /.well-known/agent-card.json (the code
  already served both; only the docs lagged).
2026-08-02 15:10:15 -07:00
Teknium 7483745da7 feat(gateway): simplex channel enumeration + show configured platforms in hermes send --list
Builds on the adapter list_channels() hook (cherry-picked from #43545 by
@Guoen0):

- plugins/platforms/simplex: implement list_channels() — enumerates
  contacts (/contacts) and groups (/groups) over the live daemon
  WebSocket into the channel directory. Returns None when the WS is
  down so the directory falls back to session discovery instead of
  wiping known targets.
- hermes send --list: merge configured-but-undiscovered platforms into
  the listing. Previously a platform configured only via env (e.g. a
  fresh SimpleX setup used for outbound sends) was silently omitted,
  leaving users guessing at platform names.
- format_directory_for_display(): accept an explicit platforms view and
  render empty platforms with a targeting hint instead of hiding them.
- docs: simplex hermes-send section.

Reported by Fedpostoffice on Discord (simplex missing from
hermes send --list; guessed platform names simplex-chat/simplex-relay).
2026-08-02 15:08:45 -07:00
Teknium 86fd6da1dc fix(hooks): single delivery_id across header+body, never follow redirects
- delivery_id is now generated once per firing and used for both the
  X-Hermes-Delivery header and the signed body's delivery_id field —
  previously they were two different uuid4s, breaking receiver-side
  dedupe as documented.
- 3xx responses are no longer followed: urllib's default redirect
  handler converts a redirected POST into a body-less GET, silently
  dropping the signed payload. Redirects now log a misconfiguration
  warning and count as delivery failure (no retry).
- Docs: receiver-side replay-protection guidance (dedupe on
  delivery_id, timestamp freshness window) + redirect semantics.
- Tests: 5xx retry count, redirect-not-followed (sabotage-verified),
  header/body delivery_id equality.
2026-08-02 15:01:11 -07:00
Teknium 3829e34e23 feat(hooks): outbound webhooks — push signed lifecycle events to external HTTP endpoints
The inverse of the inbound webhook platform: hooks.outbound in
config.yaml lists HTTP targets + the plugin-hook events they subscribe
to (on_session_end, subagent_stop, post_tool_call, ...). Each firing
POSTs a JSON payload (same top-level shape as shell hooks' stdin wire)
signed GitHub-style with HMAC-SHA256 (X-Hermes-Signature-256).

Rides the existing hook bus — notify-only callbacks registered on the
plugin manager at the same CLI/gateway/main entry points as shell
hooks. Delivery is fire-and-forget via a bounded queue + single daemon
worker thread, so a dead endpoint can never stall a tool call. Bounded
retries (5xx/conn errors once; 4xx never). secret_env preferred over
inline secret. HERMES_SAFE_MODE skips registration. hermes hooks list
shows outbound targets with signed/UNSIGNED status.

Zero new model tools, zero new subsystems.
2026-08-02 15:01:11 -07:00
teknium1 43c79cd84a feat(skills): add grounded-citations skill for verifiable sourcing
Answers and written deliverables that rest on retrieved information now get
inline numbered citations plus a mechanically-rendered Sources list, with a
persistent ledger that makes a hallucinated citation detectable.

- skills/research/grounded-citations/scripts/sources.py: stdlib citation
  ledger (add/ingest/list/render/verify) at
  $HERMES_HOME/cache/citations/ledger.json, profile-aware, O_EXCL-locked so
  parallel subagents sharing a ledger can't collide on ids
- SKILL.md: cite-while-drafting procedure, register-at-retrieval rule,
  pitfalls, verification gate
- references/citation-formats.md: per-target placement (markdown, LaTeX/PDF,
  docx, pptx, xlsx, wiki, BibTeX handoff to research-paper-writing)
- references/grounding-rationale.md: why numbered ids (ALCE 2305.14627,
  WebGPT 2112.09332, Perplexity marker conventions), and how this relates to
  the in-process registry in PR #44833
- tests/skills/test_grounded_citations_skill.py: 30 tests
2026-08-02 14:27:47 -07:00
Hermes agent 4e0a775580 fix(state): make VACUUM interval configurable 2026-08-02 21:32:13 +05:30
ethernet 713a983e4a feat(runtime)!: require Node 26 across all installers, heal, and upgrade paths
Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.

Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
  the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
  winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
  Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
  pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
  wrapper check ratchets from `>= 20` to `>= 26`.

Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
  and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
  _nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
  and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
  drives the Windows heal path's latest-v26.x download.

Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
  `^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
  acp, nix-setup, contributing). Skill docs describing third-party tools'
  own requirements are untouched.

Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.

Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
2026-08-01 21:17:51 -04:00
Zeraphim 0399711bec feat(desktop): add terminal font picker 2026-08-01 14:30:08 -07:00
Teknium 003af7f85c fix(docker): update runtime tests and docs for the entrypoint dispatcher
Follow-ups from sweeper review of #43763:
- tests/docker/test_tini_compat_shim.py asserts the dispatcher
  ENTRYPOINT (with /init delegation check) instead of a bare /init
- tests/docker/test_smoke.py gains a docker run --init regression
  for the non-PID-1 fallback (#38349)
- website/docs/user-guide/docker.md and the s6 supervision skill
  document the dispatcher and its wrapped-runtime fallback
2026-08-01 10:52:34 -07:00
Eugeniusz Gilewski 64dd865912 fix(deps): repair Google transitive security floors (#72108)
Google API and authentication packages permit vulnerable httplib2 and pyasn1
transitives, while the Workspace and Google Chat runtime installers previously
treated any importable version as sufficient. Existing environments could
therefore remain vulnerable after the project dependency pins were repaired.

Carry the fixed versions through the Google and Vertex extras, lazy feature
requirements, lockfile, and both runtime installers. Route the documented
Google Chat installation path through its maintained secure requirements
instead of an unconstrained direct pip command.

Detect stale distributions, install only unsatisfied requirements, and verify
the result before continuing. Behavioral tests cover those repair invariants
without freezing manifests, lockfiles, or complete package sets.

Related #72108
Extracted from #72840
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-31 23:18:38 -07:00
Teknium 0d9892379c docs(kanban): document profile-owned notification delivery in multi-gateway setups
Follows PR #75592: the notifier is no longer gated on
kanban.dispatch_in_gateway. Every gateway delivers events for
subscriptions owned by the profiles whose adapters it hosts; legacy
unstamped subscriptions go only through the confirmed dispatcher
lock owner. Adds a 'Multi-profile setups' subsection to the kanban
Gateway notifications docs (en + zh-Hans).
2026-07-31 22:47:49 -07:00
teknium1 9704ed86c1 feat(cli): ! shell mode — run a command without spending a model turn 2026-07-31 21:21:20 -07:00
teknium1 a55a52c72f feat(cli): complete the Ctrl+S prompt stash — keybinding, state machine, tests
Finishes the input stash started in the preceding commit from PR #4771.
That PR shipped only the panel renderer: its `@kb.add('c-s')` handler and
stash-state initialization were lost in a rebase, so the panel predicate
read undefined `_stash_panel_open` / `_stash_list` and the feature was
unreachable. This adds the missing half and the tests the PR never had.

Resolves the review feedback on #4771:

- Rebuilt the stash on current main's keybinding setup. The `c-s` key was
  unbound repo-wide, so there is no conflict.
- Extracted the state machine into `hermes_cli/prompt_stash.py` as pure
  functions (no prompt_toolkit import) so it is directly unit testable —
  the PR was cli.py-only with zero tests.
- Dropped the PR's unrelated changes: delegation `supervisor_model` /
  `execution_model` config aliases, and stale reverts of the banner
  builder, worktree pruning, logging setup, and MCP toolset validation
  that its 14k-commit-old base dragged along.
- Fixed the 📌 double-width measurement for real. Three commits in the PR
  ("subtract 1 from len()", "use bare len()", "subtract 1 again") were
  chasing this by tweaking `len()`; all horizontal math now goes through
  `_status_bar_display_width` (prompt_toolkit `get_cwidth`), which also
  keeps CJK previews inside the border. Narrow terminals fall back to
  compact header/footer labels instead of overflowing — caught by a
  parametrized width test, not by eyeballing.

Gesture (the contributor's design, kept):

- Composer has content → push onto the stash, clear the input.
- Composer empty, one stashed → pop it straight back.
- Composer empty, 2+ stashed → open the browse panel (↑↓ / Enter / D / Esc).
- Panel open → Ctrl+S closes it.

Pushing onto a stack rather than a single slot is what makes repeated
Ctrl+S safe: a second stash never silently overwrites the first, and with
2+ parked the panel asks rather than guessing which to restore. A `📌 N`
status-bar badge and a composer placeholder advertise the parked draft so
it cannot be silently forgotten.

Deliberate departures from the PR:

- No auto-restore after the agent responds, and no `display.stash_auto_restore`
  config key. The PR itself had already defaulted this to false as
  "avoids surprising the user"; a keystroke the user pressed should not
  cause text to reappear on its own, so the dead default is dropped
  rather than carried as config surface.
- Nothing is persisted to disk. Drafts routinely contain pasted
  credentials and NDA material, so the stash is session-scoped and
  in-memory only. Any future persistence must route through
  `get_hermes_home()`.
- Suppressed while a modal prompt owns the composer (sudo / secret /
  approval / clarify / slash-confirm / model picker) so Ctrl+S can never
  stash a password.
- Restoring images extends `_attached_images` instead of replacing it, so
  an attachment added since the stash was taken is not silently dropped.
- `buf.reset()` on stash (not `text = ""`) clears completion state,
  selection, and undo stack with the text.

Tests: 95 new tests across two files — 66 on the state machine (empty
buffer is a no-op, exact round-trips including newlines/tabs/CJK/fences,
no-clobber ordering, cap eviction, indicator states, panel cursor
clamping and deletion, the full resolve_ctrl_s decision table) and 29 on
the cli.py wiring (per-instance stash, keybinding registration guard,
layout slot, panel bounded at 8 widths, status-bar indicator lifecycle).
The keybinding-registration test asserts the `c-s` handler exists in
source specifically so the rebase loss that broke #4771 cannot recur.

Verified: 153 passed, 0 failed across the two new files plus
tests/cli/test_cli_init.py and tests/cli/test_cli_extension_hooks.py.
ruff check clean; check-windows-footguns clean.

Docs: Ctrl+S added to the CLI keybindings table.

Co-authored-by: CK iRonin.IT <cyprian@ironin.pl>
2026-07-31 21:21:16 -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
teknium1 4b33e5663b refactor: config auto-migration support floor at v12 + deprecated shim retirement 2026-07-29 16:44:31 -07:00
Teknium 25927884e0 docs(acp): document the Buzz Desktop model picker
Buzz Desktop v0.5.1 now renders Hermes' ACP model menu in agent runtime
settings. Add a short note under the Buzz Desktop host section explaining
where the list comes from (the shared authenticated-provider inventory),
the provider:model / custom:<name>:<model> ID shapes, and that a pick is
session-scoped rather than a Hermes-wide default change.
2026-07-29 15:21:39 -07:00
Gille 94d1dff50d fix(wake): route desktop control and select input devices 2026-07-29 14:04:21 -06:00
Francesco Bonacci c268397752 feat(computer_use): align cua-driver 0.10 permission modes 2026-07-29 12:19:37 -07:00
Teknium 5081551f09 fix(voice): full-duplex agent-turn listener — interrupt by voice during generation AND playback
Replaces the half-duplex per-playback barge monitors with ONE listener
that runs for the entire agent turn in continuous voice mode: armed at
utterance-submit, disarmed when the turn is fully done (response + TTS
finished). Fixes Teknium's live report that voice interruption never
works: (a) not while the LLM is generating, (b) not while TTS plays.

Root causes:
- HALF-DUPLEX GAP: the barge monitor only spawned when TTS playback
  STARTED (cli.py streaming/whole-file paths, gateway _tts_stream_begin).
  During LLM generation there was NO microphone listener at all.
- PLAYBACK DEAFNESS: the monitor calibrated its VAD noise floor WHILE the
  speaker was blasting TTS (speaker bleed baked into the floor), then
  multiplied it by 8x with a 1s strictly-consecutive block requirement —
  normal speech could rarely reach the trigger, and the 2s grace
  swallowed early interjections.

New model — tools/voice_mode.full_duplex_listen():
- Pre-playback calibration: quiet-room noise floor established at turn
  start and HELD through playback (never recalibrated against bleed).
- Phase-aware trigger: generation = floor x voice.barge_in_threshold_multiplier
  (new config, default 3.0, justified by synthetic-frame tests);
  playback = additionally clamped to a 1500-RMS minimum so bleed alone
  can't trip; 4000-RMS ceiling keeps speech always reachable.
- Windowed-majority detection (>=80% of a 300ms window) instead of the
  strictly-consecutive counter that reset on intra-word energy dips.
- Grace on playback ONSET only (voice.barge_in_grace_seconds, default
  down 2.0 -> 0.5) — suppresses the onset transient, not the mic.
- Debug diagnostics at every decision point (calibrated floor, per-window
  RMS above 50% of trigger, trip/no-trip, grace suppressions) — always
  logger.debug, mirrored to stderr under HERMES_VOICE_DEBUG=1.

Phase behavior (CLI cli.py + tui_gateway/server.py, same model):
- generation: speech interrupts the in-flight turn via the SAME seam the
  typed/Ctrl+C interrupt uses (agent.interrupt()), cuts any pending TTS
  pipeline so the stale reply never plays, and submits the captured
  interjection (pre-roll capture, first syllable kept) as the next turn.
- playback: cuts TTS (streaming pipeline stop + fallback speak stop
  events + file player) and submits the capture.
- stop phrase honored in BOTH phases: mid-generation 'stop' interrupts
  the turn AND ends the voice chat (stop everything).
- one listener instance spans generation -> playback (no re-arm race);
  double-arm refused (CLI _voice_fd_active / gateway _fd_listener_active).

Gateway specifics: _arm_full_duplex_listener() at _run_prompt_submit turn
start and inside _tts_stream_begin; _speak_text_with_barge registers its
(stop, done) pair in _fd_speak_pipelines so fallback speaks are cut and
tracked; _tts_stream_barge_in_monitor kept as a shim that arms the new
listener. Desktop renderer owns its own mic path (voice-barge-in.ts) and
is unaffected; if desktop backend-mic mode is used it inherits via the
gateway.

Tests: full_duplex_listen synthetic-RMS suite (speech-over-bleed trips,
bleed alone doesn't, quiet floor held through playback, grace window,
multiplier math 3x vs 8x, windowed-majority dips), CLI listener phase
tests (generation interrupt seam, playback cut, lifecycle spans phases,
double-arm, config forwarding, stop-phrase-mid-generation), gateway
generation-phase interrupt + stop-phrase tests. Generation-interrupt
test sabotage-verified.
2026-07-29 10:08:53 -07:00
Teknium 3334db67a4 docs: fold in remaining live fixes from overnight-sweep PR cluster
Salvaged from the same PR family (#50691, #59335, #67832 by @virtuadex):

- sessions.md: gateway routing index is the gateway_routing table in
  state.db; sessions.json is a legacy mirror behind
  gateway.write_sessions_json (from #59335)
- mcp.md: MCP server reads state.db first, sessions.json fallback
  (from #59335)
- slash-commands.md: /model flags --once/--session/--refresh/--provider
  + persist_switch_by_default semantics (from #67832); /reasoning
  messaging row gains level list + --global; /history timestamps note
  (from #50691)
- environment-variables.md: TERMINAL_DOCKER_ENV,
  TERMINAL_DOCKER_EXTRA_ARGS (from #50691); MEM0_MODE/HOST/USER_ID/
  AGENT_ID rewritten for the current tri-mode plugin
- cron-script-only.md: interpreter table row matches bash-from-PATH
2026-07-29 09:45:11 -07:00
virtuadex 782d0219a0 docs: sync overnight sweep with registry, gateway, curator, cron
Salvaged from #72422 by @virtuadex (conflicts resolved against current
main; superseded slash-command hunks dropped):

- SECURITY.md + SECURITY.es.md: gateway adapters live under
  plugins/platforms/<name>/, registry in gateway/platform_registry.py
- gateway-internals.md (EN + zh-Hans): key-files table rows for
  platform_registry.py and plugins/platforms/, deferred-loading section
- slash-commands.md: /reasoning full level list (max/ultra) + --global;
  CLI-only notes list gains /prompt, /pet, /hatch, /timestamps
- cron.md + cron-script-only.md: script runner accuracy — bash resolved
  from PATH with /bin/bash fallback, script paths confined to
  ~/.hermes/scripts/, provider credentials stripped via
  _sanitize_subprocess_env
- curator.md: cron-referenced skills protected from auto-archive,
  never-used grace floor
2026-07-29 09:45:11 -07:00
Teknium c6f98b0cba docs: teach providers: dict as the canonical custom-provider format
Fixes #67278. Since config v12 (see #8776), custom_providers: list is
legacy — the migration converts it to the providers: dict and the
resolver reads the dict first (runtime_provider.py). Docs still taught
the legacy list as primary.

- integrations/providers.md: all 8 YAML examples converted to
  providers: dict (field mapping code-verified: api, default_model,
  transport), one consolidated legacy-format note
- configuring-models.md, configuration.md, credential-pools.md,
  migrate-from-openclaw.md, provider-runtime.md, faq.md: examples and
  prose flipped to dict-first; legacy list noted as still-read
- adding-providers.md untouched (sole mention is a literal test
  filename)
2026-07-29 09:44:59 -07:00
Teknium 43874d1a96 docs: accuracy sweep + coverage for 2 months of shipped features
Accuracy pass (all 373 pages audited against code, 13 parallel audits):
- configuration.md: 12 stale defaults/keys (file-sync rewrite, clarify
  timeout, streaming knobs, iteration budget, TTS/STT enums)
- reference/: commands/env-vars/toolsets/tools synced with
  COMMAND_REGISTRY, argparse tree, OPTIONAL_ENV_VARS, TOOLSETS
  (28 env vars added, 3 phantom removed, mcp__ naming, webhook
  platform restricted toolset)
- features/, messaging/, developer-guide/, guides/: ~60 factual fixes
  (web_extract truncation, dashboard auth fail-closed, delegation
  blocked tools, adapter signatures, session schema v23, phantom
  Matrix env vars, hermes setup tts, auth spotify, webhook --skills)
- zh-Hans: explicit heading IDs fix 2 broken WSL2 anchors

New coverage for features shipped in the last 2 months (verified
against code before writing):
- compression.in_place, verify-on-stop (+v31/v32 migration reality),
  ${env:VAR} SecretRef, display.timestamp_format, session:compress
  hook + thread_id/chat_type fields
- /journey learning timeline, per-channel model/system-prompt
  overrides, /sessions search, clarify multi-select, -z --usage-file,
  uninstall --dry-run, config get/unset
- MCP elicitation, extra_headers, discover_models, api-server run cap,
  Bedrock cachePoint, Discord reasoning_style, Google Chat clarify
  cards, vibe reactions, resume cwd restore, Yuanbao forwarded
  messages, api_content sidecar, roaming pet, tool_progress log mode,
  WhatsApp polls/locations, kanban per-task model + lifecycle hooks
2026-07-29 08:48:05 -07:00
Teknium 068d7812a4 docs: document Hermes Relay and the desktop feature wave
- New page user-guide/messaging/relay.md: what Relay is, enrollment
  (hermes gateway enroll), capability handshake (media, native
  approval/clarify, threads, typing), config keys, troubleshooting —
  derived from gateway/relay/ + gateway_enroll.py (PRs #48147 #48242
  #71300 #71363 #71404 #71624 #69721).
- desktop.md: artifacts viewer, timeline rail, find-in-page,
  multi-window, git review/worktrees, multi-terminal + persistence,
  theme import, rebindable shortcuts, quick entry, context-usage
  popover, keep-awake, command palette, Hermes Cloud mode, memory
  graph — all verified against apps/desktop/ source.
2026-07-29 08:48:05 -07:00
Teknium 355b376225 docs(skills): regenerate skills catalog; fix generator sentence truncation
- Fix generate-skill-docs.py short-description truncation: split on
  sentence boundary (dot + space/end) instead of the first dot, which
  mangled descriptions containing dotted paths ('.hermes/plans/') or
  abbreviations.
- Regenerate all 181 skill pages + both catalog indexes + sidebar:
  adds missing pages for inspecting-hermes-desktop-dom, tldraw-offline
  (fixes broken catalog link), and pinecone-research.
- Delete orphaned kanban-codex-lane page (skill removed in #39028).
2026-07-29 08:48:05 -07:00
kshitijk4poor 24a56f027c fix(lsp): expose lsp.idle_timeout in config, harden reaper, log reaps
Follow-ups on top of @DonutsDelivery's salvaged reaper commit (#64141):

- create_from_config() now parses lsp.idle_timeout (invalid values fall
  back to DEFAULT_IDLE_TIMEOUT) — previously the constructor knob was
  unreachable from config.yaml (config exposure adapted from #36892 by
  @0xbWy and #68091 by @9miya20)
- canonical default declared in hermes_cli DEFAULT_CONFIG so config
  discovery surfaces the knob (per sweeper review note on #64980)
- reaper loop survives transient sweep errors instead of dying and
  silently re-opening the leak (gap flagged in #68091 review)
- eventlog.log_reaped(): one INFO line per sweep + clears the
  log_active announce cache so respawns re-announce at INFO
- docs: replace the stale 'no idle-timeout reaper' paragraph with the
  new lifecycle description + config reference
- tests: reuse-refresh protection (the regression teknium's sweeper
  requested on #64141), reaper-survives-error, config propagation,
  invalid-value fallback, DEFAULT_CONFIG/manager-constant sync
2026-07-29 17:11:30 +05:30
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 ba13132298 fix(voice): bare stop phrase ends the voice chat on every surface, spoken or typed
Saying OR typing a configured stop phrase (voice.stop_phrases, default
"stop") now ends the voice chat everywhere, not just classic CLI PTT:

- hermes_cli/voice.py: new explicit on_stop_phrase callback through
  start_continuous/stop_continuous. The force-transcribe path previously
  DISCARDED the stop phrase silently — with auto_restart=False the client
  re-arms the next capture, so the conversation never ended. Both halt
  paths now fire on_stop_phrase (fallback: on_silent_limit for legacy
  callers) as user intent, distinct from the no-speech timeout.
- tui_gateway/server.py: voice.record wires on_stop_phrase and emits
  voice.transcript {stop_phrase: true} after flipping HERMES_VOICE(_TTS)
  off and stopping streaming TTS — same teardown as /voice off. The TTS
  barge-in monitor stop-checks its transcript too. prompt.submit consumes
  a TYPED bare stop phrase at the server-side choke point when voice mode
  is on (returns {voice_stopped: true}, no turn starts).
- ui-tui: voice.transcript {stop_phrase} ends voice mode with a clear
  'voice chat ended' notice (distinct from the no-speech-limit message);
  submitPrompt releases the busy latch on a consumed voice_stopped reply.
- cli.py: _typed_voice_stop in process_loop — typing a bare stop phrase
  while voice mode/continuous is active ends voice mode instead of
  sending 'stop' to the agent; typed 'stop' outside voice mode is
  unchanged. Voice transcripts skip the check (already stop-checked).
- desktop: interceptsTypedVoiceStop — the composer's onSubmit ends the
  live voice conversation (same path as clicking end on the pill) when a
  bare stop command is typed with no attachments; renderer-owned loop, so
  handled client-side like the existing spoken isVoiceStopCommand.
- tools/voice_mode.py: transcribe_recording never lets the Whisper
  hallucination filter swallow a configured stop phrase (e.g. 'bye'
  configured as a stop phrase is both a hallucination-blocklist entry and
  a stop phrase — stop-phrase check now wins).

Tests: continuous-loop signal (sabotage-verified), force-transcribe stop
signal + legacy fallback, hallucination-filter ordering, typed-stop CLI
unit tests (voice on/off/longer text), prompt.submit typed-stop gateway
tests, TUI vitest for stop_phrase event handling, desktop vitest for the
typed-stop interceptor.
2026-07-29 00:01:06 -07:00
Teknium 158e9a9977 refactor: remove the claude-marketplace skill source (redundant Marketplace hub tab)
The Skills Hub 'Marketplace' tab showed a single useless entry: Anthropic
changed .claude-plugin/marketplace.json to bundle-shaped plugins whose
source is './', so all plugins collapsed to one identifier pointing at the
repo root, and the second marketplace repo (aiskillstore/marketplace) is
gone (404). Everything in anthropics/skills is already surfaced by the
GitHub tap as the Anthropic tab, making this source fully redundant.

Removes ClaudeMarketplaceSource and all wiring: source router, index
builder (crawl + floors + sort order + rate-limit messaging), extract
labels/install/URL mapping, hub UI tab, web server labels, CLI limits,
docs (en + zh), the legacy index-cache snapshot, and test fixtures.

Stale skills-index entries with source 'claude-marketplace' still install
fine: HermesIndexSource fetches via resolved GitHub paths generically.
2026-07-28 23:16:27 -07:00
Teknium 7c6caac160 docs: document dashboard session filter tabs and photon immutable-tree fallback
Follow-up docs for the July 29 salvage wave (#73865 session filtering,
#73864 photon sidecar immutable install trees).
2026-07-28 22:54:45 -07:00
Teknium f75b577b96 docs: fix 0.0.0.0 bind-default drift after dual-stack change
LINE plugin.yaml plus line/wecom-callback/msgraph-webhook/
whatsapp-cloud/teams user-guide pages still documented the old
IPv4-only 0.0.0.0 defaults; update to the dual-stack unset default.
Telegram webhook env docs live in the adapter docstring (updated with
the code change); its plugin.yaml has no webhook host entry.
2026-07-28 22:42:41 -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
Hermes Agent 06150f70af feat(photon): add native message effects 2026-07-28 22:10:57 -07:00
Hermes Agent 077c583c75 feat(photon): add native poll sending 2026-07-28 22:10:57 -07:00
Teknium 88ff722f94 docs(api-server): document profile-bound HTTP auth from #72285
The multiplexed listener now rejects the default API_SERVER_KEY on
/p/<profile>/ prefixes (fail-closed per-profile keys). Add the
multi-profile routing section with an explicit breaking-change callout
for the next release notes.
2026-07-28 21:45:44 -07:00
Teknium 07e931fcb4 feat(buzz): WebSocket inbound transport — NIP-42 auth, live DM discovery, poll fallback
Consolidates the native-transport half of PR #73636 by @ScaleLeanChris
onto the merged adapter: persistent NIP-42-authenticated Nostr WebSocket
subscription as the default inbound path (transport=auto|websocket|poll),
kind-44100 membership events for live DM discovery, since-timestamp
resume on reconnect with bounded exponential backoff, and automatic
fallback to CLI polling when the WS can't be established. Events route
through the same _handle_event() pipeline as the poll loop, so de-dupe,
mention gating, p-tag DM latching, and allow-lists behave identically on
both transports. Outbound stays on the CLI (one-shot sends never race a
WS auth handshake — his design).

E2E verified against a real in-process websockets relay: NIP-42
challenge -> signed kind-22242 AUTH (event id re-derived server-side) ->
REQ subscription -> EVENT dispatch -> clean disconnect.

Co-authored-by: ScaleLeanChris <chris@scalelean.com>
2026-07-28 21:45:34 -07:00
Teknium e79d316a04 fix(photon): persist sidecar runtime record so cron/standalone sends work
The sidecar auth token is generated at spawn (secrets.token_hex) and
existed only in the gateway process memory + sidecar child env, so
_standalone_send from cron subprocesses, hermes send, or the dashboard
structurally could not authenticate (#69960).

The adapter now writes <hermes-home>/runtime/photon-sidecar.json
({port, token, pid}, 0600, atomic tempfile+os.replace) once the sidecar
passes its /healthz readiness check, and deletes it in _stop_sidecar,
on every startup-failure path, and at disconnect so a stale record
never outlives a dead sidecar. _standalone_send falls back to the
record when PHOTON_SIDECAR_TOKEN is unset, validating the recorded pid
is alive first; a stale record yields a clear 'gateway appears to be
down' error. Docs note the gateway-must-be-running requirement and the
Photon-side shared-line initiation policy (#51897).
2026-07-28 18:21:01 -07:00
Teknium e251e78df9 feat(tools): env_passthrough allowlist for command-provider secret scrub
Command providers legitimately reference their own API keys in shell
templates (curl one-liners). The #70342 scrub removes ALL provider keys,
which would break such setups. Add a per-provider env_passthrough list
(TTS + STT) that copies named variables back from the parent env, plus
docs and tests. Scrub stays the default; passthrough is explicit opt-in.
2026-07-28 18:12:26 -07:00
Eugeniusz Gilewski b76acacbb9 fix(tools): execute local STT templates without a shell
HERMES_LOCAL_STT_COMMAND rendered quoted placeholders into a
user-configured template and passed the result to shell=True. Shell
metacharacters in the template therefore remained executable syntax even
though the placeholder values themselves were quoted.

Tokenize the rendered template and invoke it as an argv list while
preserving the existing timeout, closed stdin, and Windows creation flags.
Lock the invocation contract with metacharacter regression coverage and
document explicit shell wrapping for trusted templates that need it.

Salvages #32694

Co-authored-by: Ernest Hysa <takis312@hotmail.com>
2026-07-28 18:12:26 -07:00
Sherman 273b986fd9 feat(tools): expand command TTS output_format allowlist (m4a/aac/amr/opus)
Command-type TTS providers validated output_format against a hardcoded
{mp3,wav,ogg,flac} set; any other value was silently coerced back to mp3,
which then mismatched the output path the post-run check expects. This
blocked common ffmpeg-producible containers/codecs — notably m4a (AAC),
the portable choice for WeChat/iOS/mobile voice files — with no
config-only path (only a local source patch, lost on every update).

Widen COMMAND_TTS_OUTPUT_FORMATS to add m4a, aac, amr, opus. This only
permits a command provider to declare these; the user's command still
produces the file (e.g. via ffmpeg). No built-in provider behavior
changes and no new required config.

Update the two tests that pinned the old set, and add a positive case
covering the new formats. Document the supported output_format values.
2026-07-28 18:12:26 -07:00
CleanDev-Fix 4e8a66dace fix(tools): keep command TTS deadline through exit 2026-07-28 18:12:26 -07:00
Teknium 7e7f7d3059
Merge pull request #70509 from NousResearch/hermes/hermes-29661bf6
feat(voice): on-device wake words with open-vocabulary phrases and multi-profile voice routing
2026-07-28 17:58:33 -07:00
Rob Zehner e01d6650fe docs(user-guide): add annotated recommended default settings for Buzz adapter
Expands the 'Recommended display settings' section into a comprehensive
'Recommended default settings' block covering display, access control,
polling, and mention behavior. Each setting includes an inline annotation
and rationale bullet. Matches Telegram/email default behavior (no
intermediate tool output, mention-gated channels, private-by-default
access).

Refs #68871
2026-07-28 17:57:48 -07:00
Rob Zehner 66fc2e2a92 feat(plugin): add Buzz (Block/Nostr) platform adapter
Plugin-path adapter (zero core changes) connecting Hermes to a Buzz
community relay via the buzz CLI binary (JSON in/out, arg-list exec,
key passed via env only). Inbound uses a poll loop with per-channel
high-water marks seeded from newest (no history replay), event-id
de-dupe, self-echo suppression by pubkey, and mention gating in
channels (DMs always dispatch). Registers env_enablement, cron
home-channel delivery, and an out-of-process standalone sender,
mirroring the IRC plugin.

Verified against a live relay: connect -> send -> poll -> MessageEvent
round-trip, self-echo suppressed, clean disconnect.

Known limitation: polled inbound (default 4s); a websocket transport
(buzz-ws-client) is a future optimization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:48 -07:00
Teknium 82ed4dee36 docs(acp): verify Buzz relay-bridge docs against buzz source, cross-link modes, add zh-Hans
Follow-up on the salvaged #69915 commits:
- verified env vars against block/buzz crates/buzz-acp/src/config.rs
  (BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY, BUZZ_API_TOKEN, BUZZ_ACP_AGENT_COMMAND/
  ARGS, BUZZ_ACP_RELAY_OBSERVER, BUZZ_ACP_AGENT_OWNER) and kind 24200
  observer frames against docs/nips/NIP-AO.md
- dropped unverifiable claims: docs/hermes-agent-acp.md link (404 upstream),
  relay-directory profile publication / External-agents card walkthrough,
  'bot role' membership phrasing
- replaced key provisioning with the actual buzz-admin generate-key /
  add-member flow from the buzz-acp README
- retitled the section 'Buzz channels (relay bridge)' and cross-linked it
  with the Buzz Desktop managed-runtime section both ways; permission
  paragraph now points at the owner-only warning instead of duplicating it
- zh-Hans translation of the new section + retitle to ACP 宿主集成
2026-07-28 17:57:37 -07:00
nytemodeonly c83dadc1d3 docs(acp): add Buzz external-agent activity guidance
Co-authored-by: nytemodeonly <contact@nytemode.com>
Signed-off-by: nytemodeonly <contact@nytemode.com>
2026-07-28 17:57:37 -07:00
nytemodeonly e755f1725f docs(acp): document Buzz host integration
Co-authored-by: nytemodeonly <contact@nytemode.com>
Signed-off-by: nytemodeonly <contact@nytemode.com>
2026-07-28 17:57:37 -07:00
Brooklyn Nicholson bc1c168d49 docs(desktop): explain macOS TCC permission persistence and the keychain identity opt-in 2026-07-28 17:28:33 -05:00
Siddharth Balyan 3cedac00b7
fix(credits): reintroduce grant_spent, gated on an in-session crossing (#73634)
* Revert "fix(credits): remove the 'Grant spent · $X top-up left' notice"

This reverts commit 5dc6a14c14.

* fix(credits): reintroduce grant_spent behind an in-session crossing gate

The removed notice nagged because its condition is a steady state for
accounts living on top-up, the latch is per-session, and the cold-start seed
runs the policy at every session open — every session re-announced
'Grant spent · $X top-up left'. Reintroduce it gated so only a session that
WATCHES the grant run out announces:

- seen_grant_unspent crossing gate, mirroring seen_below_90: opens when the
  session observes the grant meaningfully unspent (>= GRANT_UNSPENT_MIN_MICROS,
  1 cent — portal-seeded states derive micros from float dollars and can carry
  sub-cent residue where headers report exactly spent). Seeds never prime it.
- The gate guards only the show branch and is consumed by the announcement —
  one announcement per crossing. Header flicker (used_fraction None and back)
  clears the sticky line but cannot re-announce; a renewal re-opens the gate.
- new_credits_latch() centralizes the latch shape (agent build, lazy re-init,
  and the policy test helper all build through it).
- Tests: steady-state-open stays silent (seed + policy seams), live crossing
  announces once, flicker/renewal/residual cases locked; the rendering test
  primes the gate and asserts its leg count so a gate regression cannot
  silently shrink coverage.
2026-07-28 21:36:23 +00:00
Atakan 8c5e846536 fix(gateway): bind HTTP auth to routed profiles 2026-07-28 14:22:18 -07:00
3ASiC 45f7030786 fix(tts): bind MiniMax credentials to their region endpoint 2026-07-28 14:07:21 -07:00
Teknium 0cf58de85e
Merge remote-tracking branch 'origin/main' into wake-toggle-config
# Conflicts:
#	tests/test_tui_gateway_server.py
#	tui_gateway/server.py
2026-07-28 12:37:35 -07:00
Kewe63 f98952b267 feat(voice): make beep notification volume configurable from config.yaml
Closes #55908. The CLI voice-mode beep amplitude is hardcoded at 0.3 inside
tools.voice_mode:play_beep(), which makes the record start/stop cues too
quiet on low-volume systems and headphones. Users couldn't adjust it
without editing source.

Move the literal into a configurable voice.beep_volume setting (clamped to
0.0-1.0, default 0.3 to preserve prior behaviour). The new
_get_beep_volume() helper reads via the same load_config() pattern used by
cli.py's _voice_beeps_enabled() and hermes_cli/voice.py's _beeps_enabled(),
keeps bools / out-of-range / non-numeric / NaN values safely on the default,
and falls back silently if config can't load so the audio cue never breaks
the voice loop on a degenerate config.yaml.

Covered by tests/tools/test_voice_mode.py:
- TestGetBeepVolume (12 cases: missing key, custom value, boundary 0.0/1.0,
  out-of-range clamp, type coercion, bool guard, NaN guard, exception
  guard, dict-typed voice section)
- TestPlayBeepVolumeWiring (guards against re-introducing a hardcoded 0.3
  literal in play_beep)

Docs: website/docs/user-guide/configuration.md mentions the new key.
Other locale translations (zh-Hans etc.) intentionally untouched —
handled by the regular i18n sync pipeline as a separate change.

No change in default behaviour: existing users hear exactly the same beep.
2026-07-28 11:57:37 -07:00