Commit Graph

637 Commits

Author SHA1 Message Date
Teknium 66ea4e686d feat(media): default-on upscaling for sub-2MP image models (FAL + Krea)
Per review: upscaling should be the default behavior (like the original
flux-2-pro chain), not agent opt-in. Policy: every image model whose
native output is below ~2MP now sets upscale=True in its catalog —
users never silently get low-res images. Native hi-res models
(Seedream 5 Pro/Lite, Krea 2 Large) stay off to avoid paying to
upscale already-large output.

- FAL catalog: 16 models flipped to upscale=True (klein, z-image,
  nano-banana pro/2/2-lite, gpt-image 1.5/2, ideogram v3/v4, recraft
  v4/v4.1, qwen image/3, krea-2 medium on FAL, MAI 2.5 pro).
- Krea plugin: per-model upscale defaults (medium + medium-turbo ON at
  1.5K native; large OFF at 2K native), precedence explicit kwarg >
  image_gen.krea.upscale config > catalog default.
- The 'upscale' tool param remains as a per-call override in both
  directions (false = fast draft, true = force on hi-res/edits).
- Video unchanged: opt-in only (default-on would double every video's
  cost and latency).
- Sibling tests updated: routing/payload tests pass upscale=False where
  the assertion targets the generation submit; catalog test now pins
  the native-resolution policy instead of the flux-2-pro snapshot.
2026-08-08 14:49:28 -07:00
Teknium 137960c9aa feat(media): opt-in upscale pass for image_generate and video_generate across FAL and Krea
The generated-media surface previously had almost no upscaler coverage:
only fal-ai/flux-2-pro chained Clarity Upscaler (hardcoded catalog
default), every other image model returned ~1MP output with no high-res
path, and video had no upscaler at all. Krea's API treats the enhancer
as a standard second pass; this brings the same shape to Hermes.

- image_generate: new optional 'upscale' boolean in the tool schema.
  Explicit true chains the backend upscaler on ANY model (including
  edits); explicit false disables flux-2-pro's automatic default;
  omitted keeps per-model catalog behavior. Response now reports
  'upscaled' so the agent knows which resolution it got.
- FAL image path: explicit flag overrides the catalog 'upscale' default
  (Clarity Upscaler, 2x). Failure falls back to the native image.
- Krea plugin: upscale=true chains Krea Enhance
  (/generate/enhance/krea/enhance, 2x, prompt-guided) through the same
  BYO/managed base URL + auth as generation, with a best-effort poll
  loop that never fails a successful generation.
- video_generate: new optional 'upscale' boolean; FAL video plugin
  chains ByteDance SeedVR2 (fal-ai/seedvr/upscale/video, 2x factor
  mode). Providers without upscalers ignore the kwarg per the ABC
  contract (documented in both ABCs).

Validation: targeted suites green (123 tests across 6 files, including
new coverage for override-wins/default-kept/failure-fallback on all
three paths); live E2E on direct FAL verified both chains end-to-end
(klein 9b + Clarity upscaled image; pixverse-v6 1s 360p + SeedVR2
upscaled video).
2026-08-08 14:49:28 -07:00
kshitij 73997c41bb fix(tts): split long speech by provider and platform limits
Salvage of PR #17973 by @TKCen (Sebastian Hänisch), re-implemented on
current main to preserve speed/instructions/provider params,
prepare_spoken_text normalization, OPUS_VOICE_PLATFORMS, is_write_denied
path security, microsecond timestamps, and the streaming-TTS gate.

- Split long TTS text into provider-safe chunks instead of truncating
- Pack generated audio against platform upload limits (Discord 10MB,
  Telegram 50MB, configurable via tts.delivery_profiles)
- Combine chunks with ffmpeg (OGG/Opus re-encoded, MP3 stream-copied)
- Multi-file delivery when combination fails or would exceed limits
- Remove hard [:4000] truncation from all callers (cli.py, voice.py,
  gateway/run.py, gateway/platforms/base.py)
- Gemini TTS raises ValueError instead of silently truncating when
  composed prompt exceeds the provider limit

Simplify-code fixes: removed dead all_touched_paths set, added
try/finally for scratch file cleanup on exception, clean error response
on chunk failure instead of leaking stale file_path.
2026-08-08 22:54:20 +05:30
Teknium 7c2bc87f81 feat(read_extract): label each unreadable PDF gap with its preceding section text
The coverage warning listed bare page ranges, which tells the agent
WHERE the gaps are but not WHAT they contain — its only options were
guessing or OCRing everything. Each gap is now labeled with the last
text extracted before it (usually a section divider page), so the agent
can decide which gaps it actually needs and render/OCR only those.
Gap list capped at 20 entries with a summary line for pathological
alternating documents.
2026-08-08 05:51:33 -07:00
Teknium a51a4cb096 fix(api-server): mark replayed tool calls completed in Responses output items
The non-streaming /v1/responses path built function_call and
function_call_output output items with no status field (and no item id),
while the SSE streaming path correctly emits status in_progress ->
completed. Spec-strict OpenAI clients reading the non-streaming output
array could interpret the status-less function_call items as pending
calls the CLIENT must execute — but these tools were already executed
server-side by the Hermes agent and are replayed for structured tool UI
only. Reported by a community user whose GPT-5.6 client concluded 'a
server should not tell an OpenAI client to execute a tool the server
already executed itself'.

- _extract_output_items now stamps status: completed and spec-shaped
  item ids (fc_/fco_) on replayed items, matching the streaming path
- test updated to pin status + id shape
- docs example updated + explicit note that output tool calls are
  replayed, never pending
2026-08-08 05:22:02 -07:00
Teknium c5f71f9a51 docs: document read_file document extraction and the scanned-PDF coverage warning 2026-08-08 05:13:46 -07:00
Teknium 1dee73400e Inspired by Cursor: fail-closed hook semantics + exit-code-2 blocking 2026-08-08 05:02:04 -07:00
Teknium 2e2fcc09ff Port from superagent-ai/grok-cli: directory-chain AGENTS.md loading 2026-08-08 04:31:45 -07:00
Teknium a978f769b1 Inspired by Cursor: MCP config context variables (${userHome}, ${workspaceFolder}, ...) 2026-08-08 03:57:00 -07:00
Ken Weiner b68a532295 fix: handle replacement transforms after CLI streaming 2026-08-08 14:06:30 +05:30
kinsolee c750d5354a fix(sessions): prevent oversized transcripts from exhausting memory 2026-08-08 13:36:08 +05:30
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
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
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
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 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 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
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
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
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
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
Teknium ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -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
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 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 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
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