FaceTicker now takes the indicator style as a prop (same value used by
busyIndicatorWidth) instead of reading the store independently, so the
rendered busy indicator and its reserved width can't desync on /indicator
changes.
Inside an s6 container, `gateway run` redirects to the supervised
gateway and then keeps the CMD process alive as a no-op heartbeat so
/init doesn't start stage-3 shutdown. That heartbeat is
`os.execvp("sleep", ["sleep", "infinity"])`, which does a PATH lookup
for the `sleep` binary. When PATH was empty/truncated/clobbered at that
point — e.g. after user customizations rewrote PATH, or on a minimal
image without `sleep` on PATH — the exec raised FileNotFoundError,
killing the CMD process and causing /init to tear down every service:
the container failed to start (issue #36208, a regression in the s6
image from 2026.5.28).
Wrap the exec in try/except OSError: on success it still replaces the
process with the cheap `sleep` heartbeat (no resident Python
interpreter, and the existing process-tree/recursion contract is
preserved); on failure it falls back to `_block_until_terminated()` —
a SIGTERM handler (clean 128+signum exit on `docker stop`) plus a
signal.pause() loop, which needs no external binary and so can't fail
on PATH state. A threading.Event().wait() fallback covers platforms
without signal.pause().
Keeping execvp as the primary path (rather than replacing it outright)
preserves the `sleep infinity` heartbeat that the docker integration
tests assert (test_gateway_run_supervised.py) and avoids leaving a
full Python interpreter resident for the container's lifetime.
Verified end-to-end on a built image: with execvp forced to fail,
_block_until_terminated() blocks cleanly instead of raising
FileNotFoundError; normal boot still runs the cheap `sleep infinity`
heartbeat; the 6 test_gateway_run_supervised.py integration tests pass.
Salvages the two community fixes for this issue — the fallback design
from #36221 (@Pluviobyte) and the signal.pause() heartbeat from #36267
(@karmeleon) — and adds regression tests for both the normal and
sleep-missing paths.
Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
Co-authored-by: karmeleon <karmeleon@users.noreply.github.com>
Closes#36208.
Reverts the shared fmtCwdBranch default (28 → 40) so it isn't an API/
behavior change for other callers, and instead passes max=28 explicitly
from the status-bar caller where the tighter cap is intended.
Collapse the three mention-parsing helpers into one _compile_mention_patterns
that handles list/string/None inputs, and inline the require_mention bool
coercion to match the signal/dingtalk convention. Same behavior, 16 fewer
lines, no per-instance state in the staticmethod.
- Render SpawnHud last in the tail so its un-budgeted (dynamic) width can
only truncate itself, never push budgeted segments past leftWidth.
- Precompute kaomoji/emoji frame widths once at module load instead of
rescanning FACES/EMOJI_FRAMES on every status render.
- Correct the tail-priority comment to match the actual fits() order
(bar, duration, compressions, voice, session count, bg, cost).
fmtDuration renders a space between units (e.g. `59m 59s`), so the flat
6-col reservation under-counted and could let the elapsed-time tail shove
the model off-screen / break the whole-segment budget. Reserve the bounded
clock width from fmtDuration itself (MAX_DURATION_WIDTH) in both the busy
indicator reservation and the tail duration budget.
* feat(desktop): session hygiene, archive, media streaming + connecting overlay
Address a batch of desktop feedback:
- Stop leaking empty "Untitled" sessions: the TUI gateway pre-created a DB
row on every session.create (i.e. every launch/draft). Persist the row
lazily on first prompt instead, and hide message-less rows in the sidebar.
- Archive/hide sessions: new `archived` column + set_session_archived, web
API (`?archived=` + PATCH archived), Ctrl/⌘-click and a context-menu item
in the sidebar, and an "Archived Chats" settings panel to restore/delete.
- Videos load via a streaming `hermes-media://` protocol instead of capped,
in-memory data URLs (16 MB limit) — bypasses the cap and supports seeking.
- Background-process completions route to the session that launched them:
the completion event now carries session_key and each poller only consumes
its own.
- Sidebar: "Group by workspace" toggle is always visible; each workspace
group gets a "+" to start a session in that directory; "New agent"/"Agents"
relabeled to "New session"/"Sessions".
- New gateway connecting overlay (ascii decode → fade out) replacing the bare
skeleton/"starting gateway" state.
* fix(desktop): bail connecting overlay on boot error
The shownRef latch kept the connecting overlay mounted behind
BootFailureOverlay after a hard boot failure. Return null on boot.error
so the failure recovery surface fully owns the screen.
* fix(desktop): address Copilot review
- /api/sessions: validate `archived` (400 on unknown) and return `archived`
as a JSON boolean instead of SQLite's 0/1.
- PATCH /api/sessions/{id}: 400 (not a misleading 404) when the body has no
updatable fields; stop conflating a no-op with "not found".
- hermes-media protocol: drop `bypassCSP` — streaming only needs
secure/standard/stream/supportFetchAPI.
- Sidebar workspace header: split the toggle and the "+" into sibling buttons
so we no longer nest interactive elements inside a <button>.
* fix(desktop): address Copilot re-review
- hermes-media protocol: restrict streaming to an audio/video extension
allowlist (415 otherwise) so it can't be used to read arbitrary local files.
- Connecting overlay: use z-[1200] instead of the non-standard z-1200 utility.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The bootstrap installer's build.rs unconditionally baked a commit pin via
`git rev-parse HEAD`, forcing every dev build to clone an exact SHA at
install time. That SHA had to be pushed to origin or the fresh-box clone
would fail.
Make the commit pin opt-in: by default build.rs bakes ONLY the detected
branch, so the installer follows that branch's HEAD at install time. Set
HERMES_BUILD_PIN_COMMIT (SHA, tag, or branch name) to bake an immutable
commit pin for reproducible/release builds; it is resolved to a SHA via
`git rev-parse --verify <ref>^{commit}` and fails loud on an unresolvable
ref. Runtime resolution already supported branch-only pins, so no changes
needed in bootstrap.rs / install_script.rs / install.ps1.
The previous reservation set the left box width but everything still
shared one flex row, so the lower-priority tail + cwd could still shrink
`ready`/model down to fragments ("re"). Pin the essentials (indicator +
model + context) in a non-shrinking group, and render the tail segments
(bar, duration, compressions, voice, session count, bg, cost) only when
the whole segment fits in the leftover space — in priority order — so
nothing truncates mid-segment and the low-value tail drops first.
Also shrink the cwd/branch label (max 40 → 28) so it stops dominating the
bar on roomy-but-not-huge terminals.
#32049 reports that under terminal.backend: docker, write_file / patch
calls to authoritative profile state (SOUL.md, memories, etc.) land on
the sandbox-local mirror at
``<HERMES_HOME>/profiles/<name>/sandboxes/<backend>/<task>/home/.hermes/...``
— a path the host Hermes process never reads. The tool reports success,
the user sees no behavior change, and on disk two divergent copies of
SOUL.md (or any other profile file) accumulate.
The existing classify_cross_profile_target guard does not catch this:
its parts[2] check sees "sandboxes" and returns None, and the path is
in-profile from the inner-mirror perspective so even a fixed version
would not fire.
Add a parallel sandbox-mirror classifier in agent/file_safety:
* classify_sandbox_mirror_target() detects the
``…/sandboxes/<backend>/<task>/home/.hermes/…`` shape via path parts.
Detection is path-shape only — backend-agnostic, does not require
the file to exist, and works regardless of which HERMES_HOME resolves.
* get_sandbox_mirror_warning() returns a model-facing warning that
names the mirror root and the inner authoritative path the agent
likely meant.
Wire both detectors through tools/file_tools._check_cross_profile_path
so the existing write_file and v4a patch call sites pick up the new
guard with no API change. The bypass kwarg (``cross_profile=True``)
remains shared between the two guards — same "I know what I'm doing"
escape valve after explicit user direction.
This is the defense-in-depth piece of the proposal in #32049 ("any
…/sandboxes/<backend>/…/home/…hermes/… path as sandbox-mirror"). It
catches the host-side speculation case where the agent writes a literal
sandbox-mirror path. The inner-container case (where the bind mount
strips the ``sandboxes/`` prefix from the agent's path view) is out of
scope for this surgical change — that requires either a dispatch-layer
host-side check before the container handoff, or the host-side
``profile_state`` / ``soul`` tool the issue also proposes.
Soft guard, NOT a security boundary — matches the existing
classify_cross_profile_target contract.
Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: Ben Barclay <ben@nousresearch.com>
The left-content reservation used a flat constant for the busy face,
but its width varies by /indicator style: kaomoji is a wide glyph plus
a rotating verb, while unicode is a bare 1-col braille spinner with no
verb. Reserve the real width via busyIndicatorWidth(style, hasDuration)
so the model stays on-screen across styles without over-reserving the
unbounded elapsed-time tail.
The status rule reserved only 8 cols for the left segments, so the
cwd + git-branch label on the right could grow until the loading
indicator, model, and context read-out were crushed to almost nothing
(sometimes collapsing to a single illegible line) on small screens.
Reverse the priority: `statusRuleWidths` now reserves the display width
of the must-keep left content (status indicator + model + context) so
the cwd/branch segment truncates first. Add `statusBarSegments(cols)`
progressive disclosure — as the terminal narrows the low-priority tail
sheds in order (cost → bg → voice → compressions → duration → context
bar), and below the bar breakpoint the context read-out collapses to a
bare token count. Status and model are always guaranteed room.
Default `minLeftContent = 0` keeps `statusRuleWidths` byte-identical for
existing callers.
The dashboard Update button's backend guard (#36263) already returns a
structured {ok:false, error:"docker_update_unsupported", message,
update_command} envelope (HTTP 200) when running in a Docker install,
instead of surfacing a raw SystemExit. But the frontend ignored that
envelope: runAction() only branched on a thrown error, so the 200 fell
through to the action-status poll, which reported a generic
"Action failed (exit 1)" toast and never showed the actual guidance.
Now runAction() inspects the update response and, on the
docker_update_unsupported case, surfaces the backend's guidance message
plus the recommended re-pull command directly (success-styled, since it's
actionable guidance — not a crash) without starting the poll.
Closes#34347.
Cron delivery to WeChat fails with 'Timeout context manager should
be used inside a task' because _api_post and _api_get use aiohttp's
ClientTimeout directly. When the cron scheduler calls send() via
asyncio.run_coroutine_threadsafe(), aiohttp cannot find a running
task and raises RuntimeError.
_upload_media, _download_bytes, and _download_remote_media already
use asyncio.wait_for() to avoid this. Apply the same pattern to
_api_post and _api_get — the two remaining iLink API helpers that
still use the raw ClientTimeout approach.
This fixes cron delivery errors seen on the WeChat platform adapter
when meyo-external cron jobs attempt to deliver output to WeChat.
The extract pipeline (extract_media/extract_images/extract_local_files +
directive strips) can reduce a non-empty tool-using response to empty
text_content with no deliverable attachment. The 'if text_content' send
guard then silently skips delivery: a 'response ready' log with no
'Sending response', no error, and the answer never reaches the user.
- A2: snapshot the pre-extract response; when extraction yields empty text
and no image/local/media attachment, deliver the recovered original from
the post-extract_media body (so a spaced MEDIA path can't leak). Applies
on ALL platforms (supersedes the Discord-only #33842 and the unsafe
raw-fallback #29499).
- A3: loud delivery invariant - a non-empty response that produces nothing
deliverable logs response_delivery_dropped at ERROR; every recovery logs
response_delivery_recovered. No silent drop survives.
- Factor a _strip_media_directives helper for the [[...]] strips; MEDIA
stripping stays owned by extract_media, whose grammar handles spaced and
quoted paths.
- Salvaged + de-scoped the #33842 test harness to all platforms; added
unrecoverable-drop and no-leak regression tests.
A streamed preamble ("Let me search...") finalized at a tool boundary
routed through _try_fresh_final, which unconditionally set
_final_response_sent=True even though it is a NON-final segment. The
gateway then reads that flag as "final delivered" and suppresses the
genuine final answer produced on the next API call, so the user silently
gets nothing. Only reproduces with fresh_final_after_seconds > 0.
- _try_fresh_final / _send_or_edit take is_turn_final; the segment-break
call site passes is_turn_final=got_done so only the turn-final answer
marks final-delivered.
- _reset_segment_state clears the final-delivery flags at every tool
boundary as defense-in-depth against any future premature setter.
- Failing-first regression + happy-path no-duplicate test.
Sourced from X/Twitter, blogs (Medium/Substack/dev.to), and YouTube since the
last refresh. Deduped against the existing 237 entries by id, url, and author.
237 -> 262 stories.
Highlights: 24/7 Mac Mini agent at $21/mo (@witcheer), automated TikTok
slideshow factory (@cyrilXBT), per-client isolated profiles as an AI-ops
business (@IBuzovskyi), PM briefing 20->8min (@aakashgupta), Railway+Telegram
deploy gotchas (Tessa Kriesel), compounding-cost field report (chintanonweb),
18-agent Kanban fleet (Tonbi), and several daily-automation setups.
Wires the salvaged search helpers into the shared curses menu driver and
turns on type-to-filter for the CLI model pickers (the 100+ model lists
that previously required scrolling).
- Search lives in the shared `_run_curses_menu` driver behind a
`searchable` flag + `search_labels`, so both `curses_radiolist` and
`curses_single_select` get it without per-menu duplication. `/` opens
the filter, BACKSPACE edits, Ctrl+U clears, ESC clears the filter then
cancels. Returned values are always original item indices.
- `_filter_indices` RANKS matches (best-first) via a Python port of the
TS scorer in ui-tui/src/lib/fuzzy.ts and web/src/lib/fuzzy.ts. The port
is byte-identical in score: same per-char bonuses, prefix (+8) and
exact (+20) bonuses, camelCase/word-boundary detection (matching on the
lowercased target, boundary on the original case), and the -len*0.01
length tiebreak — so the CLI, TUI, and WebUI rank results identically.
A cross-language parity test pins the exact scores.
- `_prompt_model_selection` (the canonical picker across the model flows)
and the custom-provider model list pass `searchable=True`.
- Split `_decode_menu_key` out of `read_menu_key` so the search loop can
peek the raw key (catch `/`) before nav decoding.
- ESC during active search now clears the query (restores the full list)
so a no-match filter can't strand the user; printable-key capture is
restricted to ASCII to avoid Latin-1 mojibake.
- Update two setup-menu tests whose mock signatures predate the new
`searchable` kwarg; add ranked-scorer + parity + state-machine tests.
Pure, refactor-independent helpers for type-to-filter search in the
curses single-/radio-select menus: subsequence matching, filtered-index
mapping, cursor reconciliation, scroll clamping, and an active-search
key handler, plus unit tests.
Salvaged from #22758 (the curses event loop was since refactored into a
shared driver on main, so the integration is rebuilt in a follow-up
commit; these pure helpers and their tests carry over unchanged).
Adds fuzzy subsequence matching with quality ranking to the model
pickers, replacing the WebUI's exact-substring filter and giving the
TUI a search where it previously had none.
- New fuzzy scorer (ui-tui/src/lib/fuzzy.ts + an identical copy at
web/src/lib/fuzzy.ts, since the two are separate TS packages with no
shared module). Matches a query as an ordered subsequence (so `g4o`
matches `gpt-4o`), scores by quality (exact > prefix > word-boundary >
contiguous > scattered) and returns matched character positions for
highlighting. Multi-token AND semantics (`clad snnt` -> claude-sonnet).
15 vitest tests cover the algorithm.
- WebUI ModelPickerDialog: ranked fuzzy filter on providers + models;
matched characters in model rows are highlighted via <mark>.
- TUI modelPicker: type-to-filter on the provider and model stages with
live ranking. Backspace edits the filter, Ctrl+U clears it, Esc clears
a non-empty filter before navigating back. Persist-global / disconnect
shortcuts moved from g/d to Ctrl+G / Ctrl+D so letters feed the filter.
Closes#30849
* fix(file_tools): block agent writes to ~/.hermes/config.yaml to prevent silent approval bypass
* fix(approval): pair terminal-side gate for ~/.hermes/config.yaml writes
Subway2023's #14639 blocks write_file/patch to ~/.hermes/config.yaml, but
the terminal side was only partially paired: echo>/tee/cp/mv to config.yaml
already tripped the project-config pattern, while `sed -i` and direct edits
slipped through with auto-approve. An unpaired write_file deny is theater per
SECURITY.md — the agent could flip approvals.mode=off via `sed -i` and the
mtime-keyed config cache reloads it mid-session.
config.yaml IS the security policy (approvals.mode/yolo/permanent allowlist
live there), so it warrants real pairing, not a half-door. Add a
_HERMES_CONFIG_PATH fragment mirroring _HERMES_ENV_PATH, fold it into
_SENSITIVE_WRITE_TARGET (covers tee/>/>>/cp/mv), and add sed -i coverage for
both config.yaml and .env. Pins 9 regression tests including no-regression
guards (reads pass, /tmp writes pass).
Co-authored-by: sbw2025 <subw3@mail2.sysu.edu.cn>
* chore(release): map Subway2023 for PR #14639 salvage
* docs: expand quickstart Skills section
The Skills section was two bare commands with no framing — it never said
what a skill is, how skills load, or what the install slug means. Expanded
to explain the concept, the bundled catalog, install/browse/use flow, and
slash-command activation. Removed the inaccurate /skills chat-command hint
(skills become individual /<name> commands; hermes skills is the CLI verb).
---------
Co-authored-by: sbw2025 <subw3@mail2.sysu.edu.cn>
Port PR #29365's tool-surface contract test: terminal/file/execute_code
already honor TERMINAL_CWD (out of scope for the resolver cluster). Pinning
the behavior makes the supersession of #29365 airtight and guards against a
future refactor silently regressing the workspace contract.
Cover the two new hardening behaviors that were unpinned: whitespace-only
TERMINAL_CWD falling through to getcwd/None, and OSError from the getcwd
fallback arm propagating to the build_environment_hints try/except guard.
Do not treat lack of application-level SimpleX events as a stale WebSocket. The websockets client already uses protocol ping/pong for connection liveness, so quiet but healthy connections should not be closed by the health monitor.
* fix(file_tools): block agent writes to ~/.hermes/config.yaml to prevent silent approval bypass
* fix(approval): pair terminal-side gate for ~/.hermes/config.yaml writes
Subway2023's #14639 blocks write_file/patch to ~/.hermes/config.yaml, but
the terminal side was only partially paired: echo>/tee/cp/mv to config.yaml
already tripped the project-config pattern, while `sed -i` and direct edits
slipped through with auto-approve. An unpaired write_file deny is theater per
SECURITY.md — the agent could flip approvals.mode=off via `sed -i` and the
mtime-keyed config cache reloads it mid-session.
config.yaml IS the security policy (approvals.mode/yolo/permanent allowlist
live there), so it warrants real pairing, not a half-door. Add a
_HERMES_CONFIG_PATH fragment mirroring _HERMES_ENV_PATH, fold it into
_SENSITIVE_WRITE_TARGET (covers tee/>/>>/cp/mv), and add sed -i coverage for
both config.yaml and .env. Pins 9 regression tests including no-regression
guards (reads pass, /tmp writes pass).
Co-authored-by: sbw2025 <subw3@mail2.sysu.edu.cn>
* chore(release): map Subway2023 for PR #14639 salvage
* fix(models): add gemini-3.5-flash to Gemini OAuth + API-key pickers
#34581 swapped gemini-3-flash-preview -> gemini-3.5-flash in the
OpenRouter and Nous lists but missed the curated Gemini catalogs, so
the Google OAuth (google-gemini-cli) picker still offered the retired
gemini-3-flash-preview slug and gemini-3.5-flash was unselectable.
Per Google's docs gemini-3-flash-preview was renamed to gemini-3.5-flash
and is served via Cloud Code Assist, so this completes the rename for:
- google-gemini-cli (OAuth/Code Assist) picker
- gemini (API-key) picker
- gemini provider default_aux_model
copilot keeps gemini-3-flash-preview (separate backend, own slug).
---------
Co-authored-by: sbw2025 <subw3@mail2.sysu.edu.cn>
M3 is 1M context, but pre-catalog builds resolved it via the generic
'minimax' catch-all (204,800) and persisted that to the context-length
cache. Step 1 of get_model_context_length returned the cached value
directly before reaching the 'minimax-m3' (1M) catalog entry, so users
who first probed M3 on an older build were stuck at 204K forever (e.g.
/new in the Telegram gateway showing 'Context: 204K tokens (detected)').
Mirror the existing Kimi/Codex stale-cache guards: when a cached entry
for a minimax-m3 slug is <= 204,800, drop it and re-resolve. M2.x slugs
(correctly 204,800) are untouched since they don't match the M3 name.
os.fchmod is Unix-only; the Windows os module has no fchmod (only
chmod). Passing mode= (e.g. 0o600 when saving the Hindsight config
during `hermes memory setup`) crashed on Windows with:
AttributeError: module 'os' has no attribute 'fchmod'
Guard the fchmod fast-path with hasattr(os, "fchmod"). Skipping it on
Windows is safe: mkstemp already creates the temp file as 0o600, and
the existing post-replace os.chmod(real_path, mode) — already wrapped
in try/except — applies the final mode durably (as far as Windows
honors it).
Adds regression tests: one simulating a Windows os module without
fchmod (must not raise), and one asserting the durable 0o600 mode on
POSIX.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the TUI exits via Ctrl+C, SIGTERM/SIGHUP, or a crash, prompt_toolkit's
teardown can be bypassed, leaving DEC 1004 (focus reporting) and 1000/1002/1003
(mouse tracking) enabled. The terminal then emits raw ESC[I/ESC[O focus events
and fragmented SGR mouse reports as visible text in whatever runs next in the
same tab.
_run_cleanup() — the once-only cleanup that runs on every catchable exit path
(atexit-registered + called on the normal/EOF/interrupt exit) — now emits
_TERMINAL_INPUT_MODE_RESET_SEQ (the same disable sequence the in-session leak
recovery already uses) as its FIRST step, so the terminal is usable immediately
on Ctrl+C and a later teardown step raising can't skip it.
The reset is gated on a new _tui_input_modes_active flag (set right before
app.run(), cleared once the modes are disabled) so non-TUI one-shot CLI runs —
which share _run_cleanup via atexit — don't emit codes for modes they never
enabled. Writes to sys.stdout when it's the terminal, else falls back to
/dev/tty. SIGKILL is uncatchable and the kanban worker's os._exit(0) bypasses
atexit, but both are non-TTY/non-TUI so there is nothing to reset there.
Adds tests/cli/test_tui_terminal_reset_on_exit.py (9): emits on a TTY when the
TUI ran, no-ops when the TUI never ran, /dev/tty fallback when stdout is
redirected, no-op when neither is available, swallows stdout errors, flag set
and cleared, and wired into _run_cleanup as the first step even when a later
step raises.
Fixes#36823
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subway2023's #14639 blocks write_file/patch to ~/.hermes/config.yaml, but
the terminal side was only partially paired: echo>/tee/cp/mv to config.yaml
already tripped the project-config pattern, while `sed -i` and direct edits
slipped through with auto-approve. An unpaired write_file deny is theater per
SECURITY.md — the agent could flip approvals.mode=off via `sed -i` and the
mtime-keyed config cache reloads it mid-session.
config.yaml IS the security policy (approvals.mode/yolo/permanent allowlist
live there), so it warrants real pairing, not a half-door. Add a
_HERMES_CONFIG_PATH fragment mirroring _HERMES_ENV_PATH, fold it into
_SENSITIVE_WRITE_TARGET (covers tee/>/>>/cp/mv), and add sed -i coverage for
both config.yaml and .env. Pins 9 regression tests including no-regression
guards (reads pass, /tmp writes pass).
Co-authored-by: sbw2025 <subw3@mail2.sysu.edu.cn>
A stream that drops mid-response after tokens are delivered (peer-closed
connection, stale-stream reconnect) is converted into a synthetic
finish_reason="length" stub. The conversation loop treated that network
stall as a max-output-tokens truncation: when the dropped content was a
tool call it retried exactly once, then hard-failed with "Response
truncated due to output length limit" — even on large-output models that
never hit any cap (e.g. Opus).
- Tool-call truncation now retries up to 3 times (was 1) with a
progressive max_tokens boost, and is stub-aware: a PARTIAL_STREAM_STUB_ID
stall prints "Stream interrupted mid tool-call — retrying (n/3)" instead
of the false "model hit max output tokens", and the give-up message
distinguishes a network drop from a real truncation.
- Length-continuation retries preserve the original request's output cap
as a floor, so a high provider/model default isn't silently downshifted
to 8K/12K on retry.
- Added _requested_output_cap_from_api_kwargs() helper.
Tests: stub-stall mid-tool-call recovery within 3 retries; continuation
preserves a large provider-default output cap.
Fixes#26425. Salvages the substance of #26427 (cap floor) and #9525
(retry bump), adapted to the post-refactor conversation_loop.py which
handles all three api_modes uniformly.
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Co-authored-by: ygd58 <ygd58@users.noreply.github.com>
* feat(dashboard): backend API for MCP, pairing, webhooks, credential pool, memory, gateway lifecycle
Adds REST endpoints so a remote admin can manage these without CLI access:
- MCP servers: list/add/remove/test (config.yaml parity with hermes mcp)
- Pairing: list/approve/revoke/clear-pending messaging codes
- Webhooks: list/subscribe/remove (hot-reloaded JSON store)
- Credential pool: list/add/remove rotation keys (via CredentialPool API)
- Memory provider: status/select/disable/reset
- Gateway lifecycle: start/stop (restart+update already existed)
Secrets redacted on read; usable values only reach the agent at session start.
All endpoints sit behind the existing dashboard auth gate.
* feat(dashboard): backend API for ops + skills hub
- Ops actions (spawned, log-tailed via /api/actions): doctor, security audit,
backup, import, checkpoints prune
- Ops reads (structured JSON): hooks list + allowlist status, checkpoints list
with per-session size
- Skills hub actions (spawned): install / uninstall / update
- Registers new action log files for all spawn-based endpoints
All gated by the existing dashboard auth middleware.
* feat(dashboard): admin pages for MCP, pairing, webhooks, and system ops
Adds four new dashboard pages + nav entries so a remote admin can manage
Hermes without CLI access:
- MCP: list/add/remove/test MCP servers
- Webhooks: list/create/delete subscriptions (one-time secret reveal)
- Pairing: approve/revoke/clear messaging pairing codes
- System: gateway start/stop/restart, memory provider + reset, credential
pool add/remove, ops (doctor/audit/backup/import/skills update) with a
live action-log viewer, checkpoints prune, shell-hooks status
api.ts: client methods + types for all new endpoints.
App.tsx: routes + sidebar nav (plain labels, no i18n key required).
Verified: tsc -b clean, production build succeeds, new pages lint clean,
zero new eslint errors in App.tsx.
* test(dashboard): cover admin API endpoints
20 tests across MCP, credential pool, memory, pairing, webhooks, ops, plus
an auth-gate parametrize that asserts every admin endpoint requires the
session token. Asserts request contract + CLI-config parity, not catalog
values (per the no-change-detector-tests rule).
* docs(dashboard): document MCP, Webhooks, Pairing, and System admin pages
Adds Pages sections for the four new admin tabs and an Admin-endpoints table
to the REST API reference. Updates the page description to reflect the
dashboard's expanded role as a full administration panel.