Follow-up on top of #65554 (@the3asic):
- Lower FTS5 'usermerge' to its minimum of 2 (persisted in the config
shadow table, applied once per SessionDB instance). Without this a
positive-rank 'merge' skips any level holding fewer than 4 segments
(SQLite FTS5 §6.8), so the fragmented-index case the cadence targets
never converges.
- Run up to _FTS_MERGE_COMMANDS_PER_PASS (4) bounded merge commands per
index per cadence, stopping early on the documented no-progress
signal (total_changes delta < 2). Each command is its own implicit
transaction, so the write lock is released between commands.
- Skip a missing messages_fts instead of raising: the chunked
optimize-storage rebuild legitimately drops + backfills FTS tables
while writers keep running; warning every 1000 writes for the whole
backfill window would be noise, and optimize_fts() has always
treated missing tables as skippable.
- Replace traced-SQL shape assertions with behavioral tests: real
fragmented-index convergence (automerge suppressed, 60 segments) and
a 3-segment below-default-usermerge compaction test that fails
against a bare positive-rank merge (sabotage-verified).
Validation on a sqlite3.backup() copy of a real 10.7 GB production
state.db (1.49M messages, 1.3 GB + 2.9 GB FTS shadow tables):
worst per-command write-lock hold 41.8 ms (was 9.2 s / 18.1 s per
index with 'optimize'), search results byte-identical, fts5
integrity-check and PRAGMA integrity_check clean, steady-state pass
0.0 ms.
Sibling of the test_auxiliary_client.py cleanup — these two assertions
pinned the OpenRouter/Nous aux default as a frozen literal and broke on
the 3.6-flash bump (CI slice 5). Reference _OPENROUTER_MODEL/_NOUS_MODEL
instead so the next default rotation can't break them.
gemini-2.5-flash shuts down Oct 16 2026 (Google deprecation schedule)
and gemini-3-flash-preview is superseded. Update every hardcoded
default to the current GA flash model:
- gemini_native_adapter: probe_gemini_tier + _create_chat_completion
default params; free-tier guidance de-pinned from a specific model's
RPD number so it doesn't stale again
- auxiliary_client: gemini/kilocode fallback aux models,
_OPENROUTER_MODEL, _NOUS_MODEL -> google/gemini-3.6-flash
(verified live on both OpenRouter and Nous portal /models)
- provider plugins: kilocode + vertex default_aux_model
- hindsight memory plugin: gemini provider default
- setup wizard gemini list: 3-flash-preview -> 3.6-flash (matches the
curated picker catalog)
- tests: aux-client assertions that pinned the old default literal now
reference the _NOUS_MODEL constant, so the next default bump can't
break them (change-detector cleanup)
Fixes#32360.
Photon (iMessage) has no real edit API for already-sent messages. When
streaming completes, the gateway attempts to edit the message to remove
the streaming cursor (▉). Without edit support, this cursor gets stuck
in the final message, corrupting Unicode characters.
This change sets SUPPORTS_MESSAGE_EDITING=False on PhotonAdapter, which
causes the gateway to suppress the streaming cursor entirely for this
platform (via _effective_cursor in gateway/run.py). This prevents the
stale tofu square (▉) from appearing in streamed iMessage responses.
Fixes#49253
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.
Port the progress-based idle-timeout pattern from _run_command_tts
(PR #50087, @CleanDev-Fix) to _run_command_stt: the timeout resets on
any stdout/stderr output, so a slow-but-alive STT provider survives
while a silently stalled one is killed. Stuck detection stays
progress-based, never wall-clock.
`transcribe_audio` reads a local file and hands it to the configured STT
provider — for the hosted providers (Groq, OpenAI, Mistral, xAI, ElevenLabs)
that ships the file's bytes to a third-party API. The same local-input read
guard was added to image-gen (587be5b5b) and xAI video-gen (104232979) to keep
the agent from feeding credential/secret stores to a provider, but STT was
missed.
Call `get_read_block_error(file_path)` at the top of `transcribe_audio`, before
validation/dispatch, so a `.env`, `auth.json`, `.anthropic_oauth.json`,
`mcp-tokens/`, etc. is refused up front instead of being transcribed (and, for
hosted providers, exfiltrated). This is defense-in-depth, not a security
boundary — the guard's own message says so — but it restores parity with the
image/video-gen tools.
Regression test: a `.env` file is refused with the shared read-guard message
before any provider dispatch (mutation-verified).
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>
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.
## Summary
- Spawn system audio players (`ffplay` / `afplay` / `aplay`) with `hermes_subprocess_env(inherit_credentials=False)`.
- Prevent gateway tokens and provider API keys from leaking into OS media helpers.
- Add a regression test asserting scrubbed env on `Popen`.
## Salvage / credit
Sibling of #70342 / incomplete #56332 (TTS/STT command scrub) on the voice-mode playback path.
Salvage incomplete #56332: route command TTS/STT through hermes_subprocess_env
while preserving delegated-child lineage, and close the sibling local-whisper
subprocess.run path that still inherited the full process environment.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up on the salvaged #71610 commits:
- acquire/release a scoped lock on relay_url+pubkey in connect/disconnect
(IRC pattern) so two profiles can't drive one Buzz identity — duplicate
replies and split de-dupe state; +2 tests
- negative-cache _resolve_user_name failures so a profile-less pubkey
doesn't re-hit 'users get' every poll sweep (flagged by @jethac on the PR)
- register user-guide/messaging/buzz in website/sidebars.ts (page was
unreachable — the #63359 trap)
On hosted relays `buzz dms list` reliably returns [] even when DM
conversations exist, so DMs leaked in via `channels list` (as entries
named "DM" with an empty description) and were seeded chat_type="group".
That put them behind the channel mention gate: "@Chip /whoami" worked
but an un-mentioned DM was silently dropped.
Classify from the Nostr tags of real traffic instead: a message another
user sends in a DM carries a structural ["p", <own pubkey>] tag even
when the text never mentions the agent, while in a real channel a
p-tag-to-self only ever accompanies a visible @mention (typed mention
or reply). A group conversation therefore latches to chat_type="dm" on
the first kind-9 event that is p-tagged to self WITHOUT a visible
mention in the content — guarded by channels-list metadata so a real
community channel (real name / non-empty description) is never
reclassified by a reply or mention that p-tags the agent.
- latch during history seeding too, so a leaked DM bypasses the
mention gate from the very first poll after connect
- keep `dms list` as a best-effort source, and scan `channels list`
as a fallback so DM conversations opened mid-run still get watched
- strip a leading @mention in DMs as well, so "@Chip /whoami" keeps
firing as a slash command after the conversation reclassifies
Refs #68871
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Port from openai/codex#34540 / #34612 ("detach non-interactive
subprocesses from stdin"): internal git invocations that run with nobody
attached — MCP catalog installs, plugin install/update, profile
distribution staging, worktree base fetches, and the desktop review
pane's git/gh backend — could hang on a credential prompt when a remote
is private, misconfigured, or requires auth. git prompts on the
inherited terminal (or via Git Credential Manager on Windows), so the
operation silently waits until its timeout, or forever at sites without
one (mcp_catalog clones have no timeout at all and inherit the parent
terminal).
- Add noninteractive_git_env() to hermes_cli/_subprocess_compat.py:
GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=Never on a copy of the
environment; GIT_ASKPASS/SSH_ASKPASS deliberately preserved so
working non-interactive auth still succeeds.
- Wire it + stdin=DEVNULL into: mcp_catalog._do_git_install (clone/
checkout), plugins_cmd (clone + pull), profile_distribution._git_clone,
web_git._git/_gh (gh also gets GH_PROMPT_DISABLED=1), and cli.py's
worktree base fetch helper.
- Tests: env contract, a real-git E2E against a local 401 Basic-auth
HTTP server proving fail-fast ("terminal prompts disabled") instead
of a hang, and per-call-site plumbing assertions. Sabotage-verified:
removing the env from web_git._git fails the site test.
Uvicorn's 16 MiB default drops one-shot base64 remote attachments before
Hermes sees them. Raise ws_max_size to fit the 256 MiB attach reader after
base64 expansion, and bump DESKTOP_BACKEND_CONTRACT to v5 so older remotes
surface skew instead of silent disconnects.
Co-authored-by: Börje <borje@dqsverige.se>
The catalog advertises every skill command but nothing about which ones
the user actually reaches for, so consumers can only sort them
alphabetically. Add a `skills` map keyed by slash command carrying the
activity count and origin (hub / bundled / local) already tracked in the
skills sidecars, read once per catalog build.
Additive: older clients ignore the field, and an unreadable sidecar
degrades to zero usage rather than failing the catalog.
Hardened-runtime restrictions are enforced even for ad-hoc signatures,
so signing with --options runtime without the allow-jit entitlements
would leave Electron/V8 crashing on launch — strictly worse than the
legacy plain ad-hoc sign. Raise instead, so the fixup falls back to the
legacy path and the bundle always stays launchable.
Local/self-updated macOS builds were finished with a plain
'codesign --force --deep --sign -', leaving a cdhash-only Designated
Requirement and stripping electron-builder's entitlements. Every rebuild
changes the cdhash, so TCC treats the new bundle as different code and
forgets Full Disk Access, Desktop/Downloads/Documents, Accessibility,
Automation, and microphone grants — users re-approve everything after
every update.
Rework the relaunch fixup to sign inside-out (standalone Mach-O
binaries, nested frameworks/helpers, then the main bundle), preserving
the repo's entitlement plists, and pin an identifier-based Designated
Requirement when signing ad-hoc so TCC has a stable identity to persist.
Opt-in desktop.macos_signing_identity names a persistent keychain cert
(self-signed Code Signing cert works — no Apple Developer account) for a
certificate-anchored DR, the strongest form. An intact Developer ID
signature is detected and never clobbered, callers can pass the
publisher-signing decision explicitly so a later dotenv load can't flip
it, and the legacy deep ad-hoc sign remains the last-resort fallback.
Co-authored-by: lewis4x4 <lewis4x4@users.noreply.github.com>
Co-authored-by: natebransc <natebransc@users.noreply.github.com>
Co-authored-by: caseyanthony <caseyanthony@users.noreply.github.com>
Co-authored-by: gvago <gvago@users.noreply.github.com>
Co-authored-by: twe-cloud <twe-cloud@users.noreply.github.com>
Real temp-HERMES_HOME tests for _broadcast_watched_changes: silent seed,
cron/sessions signature moves, the 2s sessions floor's trailing edge, the
pet signature staying 'off' without a renderable pet, meta payload on
pet.changed, and a broken probe never killing the pass. Part of #73618.
* 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.
CI (Linux) failed with ModuleNotFoundError: portalocker — it's a
win32-only dependency (concurrent-log-handler chain). Tests now lock
handles via a platform helper (fcntl on POSIX, portalocker on Windows),
mirroring production _try_acquire_mcp_discovery_lock.
Detached background delegation batches (_batch_runner) no longer honor
the foreground parent's interrupt flag — a busy-submit interrupt in the
TUI/desktop previously fabricated 'interrupted' results for background
children that should outlive the turn. Explicit cancellation still works
via _batch_interrupt.
Rebased onto current main from PR #65040; both interrupt-suppression
regression tests aligned with the current _session test helper.
Original work by @AtakanGs in #65040.
The autouse _audio_playback_guard from this PR stubs voice.speak_text
globally — but these tests exercise speak_text itself with their own
playback stubs (no real audio possible). Mark real_audio_playback so
the guard yields; the tests' own monkeypatches keep speakers silent.
Main's streaming path now resolves providers via
tools.tts_streaming.resolve_streaming_provider rather than constructing an
ElevenLabs client inline; stub the registry resolver instead so the test is
provider-agnostic and hermetic.