Commit Graph

6067 Commits

Author SHA1 Message Date
teknium
ebad6d3f1e chore(release): map yoimexex@gmail.com -> Yoimex 2026-04-26 19:02:55 -07:00
Teknium
87610ce380 fix(tools): coerce quoted use_gateway in image_gen UI detection
Follow-up to #15960 — the provider-active detection in tools_config.py
also read use_gateway with raw truthiness (is False, not dict.get), so
quoted 'false' caused the FAL-direct row to show wrong active status in
the hermes tools picker. Route both sites through is_truthy_value().
2026-04-26 19:02:55 -07:00
Yoimex
f66ebe64e8 fix(cli): coerce use_gateway config flags in tool routing 2026-04-26 19:02:55 -07:00
Teknium
36b13709f5 chore(release): map johnncenae in AUTHOR_MAP 2026-04-26 19:01:50 -07:00
Teknium
77d4766602 fix(gateway): clear pending model note on auto-reset paths too
PR #16013 plugged the leak in `/new`, but two sibling session-boundary
resets had the same bug:

1. Inactivity / suspended-session auto-reset (top of `_handle_message`)
   previously cleared only reasoning. Now drops model override and the
   queued "/model switched" note as well.
2. Compression-exhaustion auto-reset now also drops the pending note
   alongside the existing model/reasoning cleanup.

All three session-boundary sites now use the identical cleanup idiom.
2026-04-26 19:01:50 -07:00
johnncenae
00c6480a05 fix(gateway): clear stale pending model note on session reset 2026-04-26 19:01:50 -07:00
helix4u
88a85d30c1 fix(logging): attach gateway log after cli init 2026-04-26 19:01:26 -07:00
simbam99
cebf95854b Fix MessageDeduplicator max_size enforcement 2026-04-26 18:51:51 -07:00
Teknium
34eb1aaa9a fix(update): use npm ci to stop rewriting package-lock on every update (#16295)
`npm install --silent` (used by `_build_web_ui` and `_update_node_dependencies`)
silently rewrites package-lock.json on npm ≥ 10 (strips "peer": true etc.),
leaving the working tree dirty after every `hermes update`. The next update
then detects the dirty lockfile and stashes it — producing a trail of
hermes-update-autostash entries for web/package-lock.json, ui-tui/package-lock.json,
and root package-lock.json.

Switch to `npm ci` (strict, lockfile-preserving) via a new
`_run_npm_install_deterministic` helper that falls back to `npm install`
when the lockfile is missing or out of sync (WIP forks).

Verified locally: all three lockfiles stay byte-identical after the real
_build_web_ui / _update_node_dependencies run twice back-to-back. Fallback
path tested with a deliberately out-of-sync lockfile and a no-lockfile case.
2026-04-26 18:51:31 -07:00
Teknium
ab6879634e yuanbao platform (#16298)
Co-authored-by: loongzhao <loongzhao@tencent.com>
2026-04-26 18:50:49 -07:00
Teknium
5eb6cd82b2 fix(sessions): /save lands under $HERMES_HOME, widen browse+TUI picker, force-refresh ollama-cloud on setup (#16296)
Four independent session-UX bugs reported by an external user (#16294).

/save wrote hermes_conversation_<ts>.json to CWD — invisible to
'hermes sessions browse' and easy to lose. Snapshots now write under
~/.hermes/sessions/saved/ and the command prints the absolute path plus
a 'hermes --resume <id>' hint for the live DB-indexed session.

'hermes sessions browse' default --limit raised from 50 to 500. With the
old ceiling, users with moderately long histories saw only the most
recent 50 rows and assumed older sessions had been lost.

TUI session.list (`/resume` picker) switched from a hardcoded allow-list
of 13 gateway source names to a deny-list of just { 'tool' }. Sessions
tagged acp / webhook / user-defined HERMES_SESSION_SOURCE values and
any newly-added platform now surface. Default limit 20 → 200.

ollama-cloud provider setup passes force_refresh=True to
fetch_ollama_cloud_models() so a user entering their API key sees the
fresh catalog (e.g. deepseek v4 flash, kimi k2.6) immediately instead
of waiting up to an hour for the disk cache TTL to expire.

Closes #16294.
2026-04-26 18:49:48 -07:00
Teknium
7e3c8a31f0 feat(skills/airtable): tailor skill to Hermes idioms + expand cookbook
Expand the airtable skill from bare CRUD to a full Hermes-shaped
cookbook matching the linear/notion neighbors, and trim the
description to fit the 60-char system-prompt cutoff.

Hermes-specific additions:
- Explicit 'use the terminal tool with curl — not web_extract or
  browser_navigate' guidance, matching the same note in linear.
- Note that AIRTABLE_API_KEY flows from ~/.hermes/.env into the
  subprocess automatically via env_passthrough, so curl calls don't
  need to re-export it.
- Prefer 'python3 -m json.tool' (always present) over jq (optional)
  for pretty-printing, with -s on every curl to keep output clean.
- Read-before-write workflow that resolves record IDs via
  filterByFormula instead of guessing.

Cookbook expansion (new vs original):
- Field-type reference table (text, select, multi-select, attachment,
  linked record, user) with the exact write-shape Airtable expects.
- typecast flag for auto-coercing values / auto-creating select options.
- performUpsert PATCH for idempotent sync by merge field.
- Batch create/delete endpoints (10-record cap per call).
- Sort + fields query params with URL-encoding (%5B / %5D).
- Named-view query that applies saved filter/sort server-side.
- Full pagination loop template (while loop with offset).
- Common filterByFormula patterns (exact match, contains, AND/OR,
  date comparison, NOT empty).
- Rate-limit backoff guidance (Retry-After header, per-base budget).
- Airtable error-code reference (AUTHENTICATION_REQUIRED,
  INVALID_PERMISSIONS, MODEL_ID_NOT_FOUND,
  INVALID_MULTIPLE_CHOICE_OPTIONS) so the agent can map failures to
  user-actionable fixes instead of just retrying.

Also: description trimmed from 183 chars (truncated to 60 in system
prompt, losing 'filter/upsert/delete' trigger terms) down to 59 chars
that render whole: 'Airtable REST API via curl. Records CRUD, filters,
upserts.' Catalog row updated to match.

SKILL.md grew from 115 to 228 lines — still under the 500-line soft
cap and below the linear skill (297 lines) which serves the same
role for GraphQL.
2026-04-26 18:45:15 -07:00
Teknium
0bef0b9416 chore: docs + attribution for airtable skill
- scripts/release.py: map sonoyuncudmr@gmail.com -> Sonoyunchu so the
  check-attribution CI job and release notes credit Soynchu correctly.
- website/docs/reference/skills-catalog.md: add the airtable row to
  the productivity bundled-skills table.
2026-04-26 18:45:15 -07:00
Teknium
55e9329ee6 feat(config): register bundled-skill API keys in OPTIONAL_ENV_VARS
Adds NOTION_API_KEY, LINEAR_API_KEY, TENOR_API_KEY, and AIRTABLE_API_KEY
to OPTIONAL_ENV_VARS so:

- They persist to ~/.hermes/.env via save_env_value like every other
  key Hermes knows about, instead of being ad-hoc variables the user
  has to hand-edit the dotfile for.
- load_env() / reload_env() populate os.environ from .env on every
  startup — the user sets the key once, skills keep working across
  restarts without losing access.
- hermes setup / hermes config show surface them as known optional
  vars with the correct signup URL (linear.app/settings/api,
  airtable.com/create/tokens, etc.).

These four entries use category="skill" (new) rather than "tool".
tools/environments/local.py auto-adds every category=tool/messaging
entry to _HERMES_PROVIDER_ENV_BLOCKLIST, which stops env passthrough
from leaking provider credentials into the execute_code sandbox
(GHSA-rhgp-j443-p4rf). Skill API keys are the opposite case — the
point is for the agent's subprocess to see them so curl can read
Authorization headers — so they must be outside the blocklist. The
new category is inert for that check.

All four entries are advanced=True: they show up in 'hermes config'
and 'hermes status' displays, but do not nag users who have never
touched those skills during setup checklists.

E2E verified: save_env_value → reload_env → os.environ populated →
skill_view reports setup_needed=False → env_passthrough registers
the key for subprocess inheritance.
2026-04-26 18:45:15 -07:00
Teknium
0d4247d9bf fix(skills/airtable): use .env credential pattern matching notion/linear
Convert the airtable skill from 'skills.config.airtable.api_key'
(config.yaml, wrong bucket for a secret) to 'prerequisites.env_vars:
[AIRTABLE_API_KEY]' (~/.hermes/.env), matching every other bundled
skill that authenticates with an API token.

Why the original shape was wrong:
- metadata.hermes.config is for non-secret skill settings (paths,
  preferences) per references/skill-config-interface.md. Storing a
  bearer token under skills.config.* also triggered the documented
  'hermes config migrate' nag-on-every-run problem.
- The Quick Reference's 'AIRTABLE_API_KEY=...' bash line couldn't
  read skills.config.airtable.api_key anyway — it's a yaml path, not
  an env var.

Follow-up polish on the same pass:
- Added version/author/license frontmatter to match notion/linear.
- Added prerequisites.commands: [curl].
- Setup section now specifies the PAT format (pat...) that replaced
  legacy 'key...' API keys in Feb 2024, plus the three required scopes
  (data.records:read/write, schema.bases:read) and the per-base Access
  list requirement.
- Clarified PATCH vs PUT and pagination (100 records/page cap).
- Swapped verification from 'hermes -q ...' (non-deterministic) to a
  curl /v0/meta/bases call that returns a verifiable HTTP status code.
2026-04-26 18:45:15 -07:00
Sonoyunchu
c997183f53 feat(skills): add bundled Airtable productivity skill 2026-04-26 18:45:15 -07:00
Teknium
f01e4402a9 chore(release): map georgeglessner in AUTHOR_MAP 2026-04-26 18:43:57 -07:00
George Glessner
5b5a53a155 fix(cli): check hermes_cli/web_dist/ not web/dist/ for build staleness
_web_ui_build_needed() in PR #14914 checked web_dir/"dist" as the
sentinel, but vite.config.ts sets outDir: "../hermes_cli/web_dist" so
the build output lands in hermes_cli/web_dist/, never in web/dist/.
The sentinel was therefore always missing → _web_ui_build_needed always
returned True → npm install + Vite build ran on every startup → OOM on
low-memory VPS persisted unchanged.

Fix: derive dist_dir as web_dir.parent / "hermes_cli" / "web_dist" so
the sentinel points to the actual build output directory.

Fixes #14898
2026-04-26 18:43:57 -07:00
Teknium
90c84c6dba fix(gateway): unblock update subprocess on recognized-command bypass
When the gateway intercepts a pending /update prompt and the user sends
a recognized slash command (/new, /help, ...), the command now dispatches
normally AND the detached update subprocess is unblocked by writing a
blank .update_response. _gateway_prompt reads '' → strips → returns the
prompt's default (typically a safe 'n' / skip), so the update process
exits cleanly instead of blocking on stdin until the 30-minute watcher
timeout.

Also clears _update_prompt_pending[session_key] on this path so stray
future input for the same session isn't re-intercepted.

Extends PR #15849 with tests for the new cancel-write + a regression
test pinning the legacy behavior of unrecognized /foo slash commands
still being consumed as the response.
2026-04-26 18:39:44 -07:00
Yukipukii1
bdaf56a94d fix(gateway): bypass slash commands during pending update prompts 2026-04-26 18:39:44 -07:00
Teknium
bdc1adf711 chore(release): map haru398801, badgerbees, xnbi in AUTHOR_MAP 2026-04-26 18:33:35 -07:00
Badgerbees
55f212a7a2 fix(slack): honor NO_PROXY for Slack transport 2026-04-26 18:33:35 -07:00
Xnbi
7eaad06a87 fix(gateway): default Slack tool_progress to off
Slack Bolt posts are not editable like CLI spinners; medium-tier new still emitted a permanent line per tool start (issue #14663).

- Built-in slack default: off; other tier-2 platforms unchanged.

- Adjust /verbose isolation test for off to new cycle.

- Migration tests: read/write config.yaml as UTF-8 (Windows locale).
2026-04-26 18:33:35 -07:00
haru398801
a01e767b24 fix(gateway): respect config.yaml slack.enabled when SLACK_BOT_TOKEN env var is set
Previously, setting SLACK_BOT_TOKEN in .env would unconditionally enable
the Slack gateway adapter regardless of `slack.enabled: false` in config.yaml.
This caused spurious "SLACK_APP_TOKEN not set" errors when the token was
used only by skills (e.g. cron jobs that send Slack messages) rather than
for the Hermes messaging gateway.

Now, enabled: false in config.yaml is respected — the token is stored so
skills can still use it, but the gateway adapter is not activated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:33:35 -07:00
hharry11
fd474d0f00 fix(gateway): avoid cross-user mirror writes in per-user group sessions 2026-04-26 18:31:24 -07:00
Teknium
cd2aee36ca test(sessions): wire sessions_dir through auto-prune + file-cleanup regression tests
- TestAutoMaintenance gains 3 tests: auto-prune deletes transcript files
  when sessions_dir is passed, preserves them when it isn't (backward-
  compat), and never touches active-session files during prune.
- FakeDB helpers in test_sessions_delete.py accept **kwargs so they
  don't break when delete_session signature gains sessions_dir.
2026-04-26 18:31:07 -07:00
Yang Zhi
3b60abb6bb fix(sessions): delete on-disk transcript files during prune and delete (#3015)
`delete_session()` and `prune_sessions()` only removed SQLite records,
leaving .json/.jsonl transcript files on disk forever. Over time this
causes unbounded disk growth (~27MB/day observed).

Changes:
- Add `_remove_session_files()` static helper that cleans up
  `{session_id}.json`, `.jsonl`, and `request_dump_{session_id}_*.json`
- `delete_session()` accepts optional `sessions_dir` param and removes
  files for the deleted session and its children
- `prune_sessions()` accepts optional `sessions_dir` param and removes
  files for all pruned sessions after the DB transaction
- Wire up CLI `hermes sessions delete` and `hermes sessions prune` to
  pass `sessions_dir`
- File cleanup is best-effort (OSError silenced) so DB operations are
  never blocked by filesystem issues
- Fully backward-compatible: `sessions_dir=None` (default) preserves
  existing behavior
2026-04-26 18:31:07 -07:00
Wysie
0ba6471dd1 fix: recover hindsight embedded daemon after idle shutdown 2026-04-26 18:29:11 -07:00
Yukipukii1
7317d69f19 fix(security): treat quoted false as false in browser SSRF guards 2026-04-26 18:27:13 -07:00
Teknium
2a0fc97c76 chore(release): map mewwts in AUTHOR_MAP 2026-04-26 18:25:41 -07:00
mewwts
8fb861ea6e feat(gateway/slack): support channel_skill_bindings
Extends the existing channel_skill_bindings mechanism (previously
Discord-only) to Slack, so a channel or DM can auto-load one or more
skills at session start without relying on the model's skill selector
for every short reply.

Motivation: Mats's German flashcards DM pushes a cron-driven card
5x/day; he responds with one-word guesses like 'work'. Previously each
reply required the main agent to decide whether to load german-flashcards
(full opus turn just to pick a skill). With the binding configured per
Slack channel, the skill is injected at session start and grading runs
directly.

Changes:
- Extract resolve_channel_skills() from DiscordAdapter._resolve_channel_skills
  into gateway.platforms.base (now shared across adapters).
- DiscordAdapter._resolve_channel_skills delegates to the shared helper
  (behavior preserved — existing test suite still passes unchanged).
- SlackAdapter: resolve channel_skill_bindings on each message and attach
  auto_skill to MessageEvent. gateway/run.py already handles auto-skill
  injection on new sessions; this just wires Slack through it.
- gateway/config.py: accept channel_skill_bindings in slack: block of
  config.yaml (was Discord-only).
- Tests: new tests/gateway/test_slack_channel_skills.py with 11 cases
  covering DM/thread/parent resolution, single-vs-list skills, dedup,
  malformed entries. Discord suite unchanged.
- Docs: add 'Per-Channel Skill Bindings' section to Slack user guide.

Config example:
  slack:
    channel_skill_bindings:
      - id: "D0ATH9TQ0G6"
        skills: ["german-flashcards"]
2026-04-26 18:25:41 -07:00
Teknium
635253b918 feat(busy): add 'steer' as a third display.busy_input_mode option (#16279)
Enter while the agent is busy can now inject the typed text via /steer —
arriving at the agent after the next tool call — instead of interrupting
(current default) or queueing for the next turn.

Changes:
- cli.py: keybinding honors busy_input_mode='steer' by calling
  agent.steer(text) on the UI thread (thread-safe), with automatic
  fallback to 'queue' when the agent is missing, steer() is unavailable,
  images are attached, or steer() rejects the payload. /busy accepts
  'steer' as a fourth argument alongside queue/interrupt/status.
- gateway/run.py: busy-message handler and the PRIORITY running-agent
  path both route through running_agent.steer() when the mode is 'steer',
  with the same fallback-to-queue safety net. Ack wording tells users
  their message was steered into the current run. Restart-drain queueing
  now also activates for 'steer' so messages aren't lost across restarts.
- agent/onboarding.py: first-touch hint has a steer branch for both
  CLI and gateway.
- hermes_cli/commands.py: /busy args_hint updated to include steer,
  and 'steer' is registered as a subcommand (completions).
- hermes_cli/web_server.py: dashboard select widget offers steer.
- hermes_cli/config.py, cli-config.yaml.example, hermes_cli/tips.py:
  inline docs updated.
- website/docs/user-guide/cli.md + messaging/index.md: documented.
- Tests: steer set/status path for /busy; onboarding hints;
  _load_busy_input_mode accepts steer; busy-session ack exercises
  steer success + two fallback-to-queue branches.

Requested on X by @CodingAcct.

Default is unchanged (interrupt).
2026-04-26 18:21:29 -07:00
Teknium
87477756fd chore(release): map Ito-69 in AUTHOR_MAP 2026-04-26 18:21:20 -07:00
Ivan Tonov
930494d687 fix(cron): reap orphaned MCP stdio subprocesses after each tick
MCP stdio servers are spawned via the SDK's stdio_client, which on
Linux uses start_new_session=True (setsid).  When a cron job is
cancelled mid-way (timeout, agent finish, exception), the subprocess
often escapes the SDK's teardown and survives as a session leader.
Because setsid() detaches the child from the gateway's process group
/ cgroup tree, systemd does not reap it on service restart either —
so every cron tick that touches an MCP tool leaks a dangling server
process.

Fix:

* tools/mcp_tool.py — _run_stdio now wraps the whole stdio+session
  context in try/finally.  On any exit path (clean, exception,
  cancellation), PIDs still alive are moved from the active
  _stdio_pids set into a new _orphan_stdio_pids set.  Orphan
  detection is done via os.kill(pid, 0) — a cheap liveness probe
  that never signals the target.

* tools/mcp_tool.py — _kill_orphaned_mcp_children gains an
  include_active=False flag.  Default behaviour now only reaps the
  orphan set so concurrent sessions (other parallel cron jobs or
  live user chats) are never disrupted.  The existing shutdown path
  passes include_active=True to keep the previous "kill everything"
  semantics after the MCP loop is stopped.

* cron/scheduler.py — the cleanup hook is moved from run_job()'s
  finally (which would race with parallel siblings after #13021)
  into tick() after the ThreadPoolExecutor has joined every future.
  At that point there are no in-flight sessions from this tick, so
  sweeping the orphan set is always safe.

Net effect: zero regression for healthy sessions, and orphan MCP
servers no longer accumulate between gateway restarts.

Made-with: Cursor
2026-04-26 18:21:20 -07:00
Teknium
5db6db891c chore(release): map ghostmfr in AUTHOR_MAP 2026-04-26 18:20:17 -07:00
ghostmfr
e818ec520a fix(slack): harden attachment handling
Multiple overlapping Slack attachment improvements:

1. Upload retry with backoff on transient errors (429, 5xx, connection
   reset, rate_limited, service unavailable). New _is_retryable_upload_error
   helper covers three upload paths: _upload_file, send_video,
   send_document. Up to 3 attempts with 1.5s * attempt backoff.

2. Thread participation tracking: successful file uploads now add the
   thread_ts to _bot_message_ts, mirroring how text replies are tracked.
   This lets follow-up thread messages auto-trigger the bot (same
   engagement rules as replied threads).

3. Thread metadata preservation in the image redirect-guard fallback
   (send_image → send text fallback) and in two gateway.run.py send
   paths (image + document fallback calls).

4. HTML response rejection in _download_slack_file_bytes. Parallels
   the existing check in _download_slack_file. Guards against Slack
   returning a sign-in / redirect page as document bytes when scopes
   are missing, so the agent doesn't get HTML-as-a-PDF.

5. File lifecycle event acks (file_shared / file_created / file_change).
   These events arrive around snippet uploads. Acking them silences the
   slack_bolt 'Unhandled request' 404 warnings without changing behavior.

6. Post-loop message type classification so a mixed image+document upload
   classifies as PHOTO (or VOICE if no image), falling back to DOCUMENT.
   Previously, the per-file classification in the inbound loop could be
   overwritten unpredictably.

7. Expanded text-inject whitelist in inbound document handling to cover
   .csv, .json, .xml, .yaml, .yml, .toml, .ini, .cfg (up to 100KB) so
   snippets and config files are directly visible to the agent, not just
   cached as opaque uploads. Paired with new MIME entries in
   SUPPORTED_DOCUMENT_TYPES in base.py.

Squashed from two commits in #11819 so the single commit carries the
contributor's GitHub attribution (the original commits were authored
under a local dev hostname).
2026-04-26 18:20:17 -07:00
Teknium
b16f9d438b feat(telegram): send fresh finals for stale preview streams (port openclaw#72038) (#16261)
Ports openclaw/openclaw#72038 to hermes-agent.

Telegram's `editMessageText` preserves the original message timestamp,
so a long-running streamed reply (reasoning models that take 60+ seconds
to finish) would keep the first-token timestamp even after completion.
Users can't tell how long a task actually took.

When a preview message has been visible for >= 60s (configurable via
`streaming.fresh_final_after_seconds`), finalize by sending a fresh
message instead of editing in place, then best-effort delete the stale
preview. Short previews still edit in place (the existing fast path).

Implementation notes adapted from OpenClaw's TypeScript original:
- `StreamConsumerConfig` gains `fresh_final_after_seconds` (default 0 =
  legacy edit-in-place). Gateway-level `StreamingConfig` defaults to 60.
- `GatewayStreamConsumer` tracks `_message_created_ts` at first-send and
  checks it in `_send_or_edit` on `finalize=True`. New helpers
  `_should_send_fresh_final` + `_try_fresh_final`.
- `BasePlatformAdapter` gains optional `delete_message(chat_id, message_id)`
  returning False by default. `TelegramAdapter` implements it via
  `_bot.delete_message`.
- `gateway/run.py` only enables fresh-final for `Platform.TELEGRAM`;
  other platforms ignore the setting (they don't have the stale-edit
  timestamp problem or edit-then-read works cheaply).
- Fallback to normal edit on any fresh-send failure — no user-visible
  regression if Telegram rate-limits a send or the message is gone.

Tests: 15 new cases in tests/gateway/test_stream_consumer_fresh_final.py
covering short/long previews, config plumbing, delete-support absent,
send-failure fallback, __no_edit__ sentinel safety, and StreamingConfig
round-trip.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-04-26 17:26:37 -07:00
Teknium
755a280424 chore(release): map Wang-tianhao in AUTHOR_MAP 2026-04-26 13:02:51 -07:00
Wang-tianhao
6087e04043 fix(slack): extract rich_text quotes/lists and link unfurl previews
Slack's modern composer sends messages with a 'blocks' array that
contains rich_text elements. When a user forwards or quotes another
message, the quoted content shows up in the rich_text_quote children
of that array — and is NOT included in the plain 'text' field. The
agent saw only the lossy plain text and was blind to forwarded /
quoted content. Same story for link unfurl previews (Notion, docs,
GitHub, etc.) which Slack puts in the 'attachments' array.

Two fixes in the inbound handler:

1. _extract_text_from_slack_blocks walks rich_text / rich_text_quote /
   rich_text_list / rich_text_preformatted trees and renders readable
   text ('> quoted', '• bullet', code fences), dedupes against the
   plain text field, and appends the extracted content so the agent
   sees everything.

2. Link unfurl / attachment preview extraction reads title, url,
   body, and footer from the 'attachments' array and appends a
   '📎 [title](url)\n   body\n   _footer_' section per preview.
   Skips is_msg_unfurl to avoid echoing our own Slack replies back.

Routing is careful not to trust augmented text: mention gating
(is_mentioned) and slash-command detection both run against the
original 'text' field, so forwarded content containing '<@bot>' or
'/deploy' in a quote can't trick the bot into responding in a
channel it shouldn't or classifying a normal message as a command.

Adjustment from original PR: dropped _serialize_slack_blocks_for_agent,
which inlined a redacted JSON dump of non-rich_text blocks (section,
accessory, actions, etc.) — the agent would see the raw Block Kit
structure for UI-heavy alerts. It added up to 6000 characters to the
prompt context on every qualifying message with no opt-out. The
rich_text extraction and attachment unfurls cover the common bug-fix
case (quoted/forwarded content + link previews) without the prefill
tax. If a user needs block inspection later, it can return as a
config opt-in.

Also updates the Slack platform notes in session.py to accurately
describe what the gateway inlines.
2026-04-26 13:02:51 -07:00
Teknium
4921b26945 fix(cron): keep homeassistant toolset enabled when HASS_TOKEN is set (#16208)
After #14798 made cron honor per-platform `hermes tools` config, the
`_DEFAULT_OFF_TOOLSETS` filter silently stripped `homeassistant` from
cron jobs for users who'd been relying on the previous blanket toolset.
Norbert's HA cron reports regressed as a result.

The HA toolset is already runtime-gated by its `check_fn` (requires
HASS_TOKEN to register any tools). When HASS_TOKEN is set the user has
explicitly opted in — `_DEFAULT_OFF_TOOLSETS` adds nothing in that case,
so stop double-gating and restore HA for cron / cli / other platforms
without an explicit saved toolset list.

moa and rl stay off by default (original #14798 goal preserved).

Fixes HA cron regression reported by Norbert.
2026-04-26 12:55:58 -07:00
Teknium
822b507a72 chore(release): map maxims-oss in AUTHOR_MAP 2026-04-26 12:54:46 -07:00
maxims-oss
18beb69b49 fix(memory): close embedded Hindsight async client cleanly
HindsightEmbedded.close() delegates to its sync client.close(). When Hermes
created/used that client on the shared async loop, closing it from the main
thread raises 'attached to a different loop' before aiohttp releases the
session — so the ClientSession / TCPConnector leak past provider teardown.

Close the embedded inner async client on the shared loop first via
_run_sync(inner_client.aclose()), then let the wrapper's sync close()
do its daemon/UI bookkeeping.

Salvage of #14605: test placement rebased — appended TestShutdown class
after TestSharedEventLoopLifecycle (which landed on main after the PR was
written). Original author attribution preserved.
2026-04-26 12:54:46 -07:00
Tranquil-Flow
bf05b8f4a2 fix(gateway): clean up cached agents on shutdown (#11205) 2026-04-26 12:51:53 -07:00
Zainan Victor Zhou
778fd1898e fix(slack): surface attachment access diagnostics
Translate Slack attachment failures into actionable user-facing notices
instead of generic download errors. When a scope/auth/permission issue
breaks attachment processing, the user sees:

  [Slack attachment notice]
  - Slack attachment access failed for photo.jpg. Missing scope:
    files:read. Update the Slack app scopes/settings and reinstall
    the app to the workspace.

Two helpers do the translation:

  _describe_slack_api_error — handles SlackApiError responses
    (missing_scope, invalid_auth, file_not_found, access_denied, etc.)

  _describe_slack_download_failure — handles httpx.HTTPStatusError
    (401/403/404) and Slack-returns-HTML-sign-in fallbacks

Wired into three existing call sites:
 - the Slack Connect files.info path (PR #11111) so scope errors
   surface instead of being logged as generic "files.info failed"
 - the image, audio, and document download paths so 401/403 and
   HTML-body responses translate into actionable notices

Adjustment from original PR: dropped _probe_slack_file_access_issue,
the proactive pre-download files.info probe. It added one extra
Slack API call per attachment even on healthy ones, and overlapped
with the existing files.info call from PR #11111. The post-failure
translation path covers the same user-facing diagnostic value
without the per-message tax.

Also documents files:read scope more prominently in the Slack setup
guide and troubleshooting table.

Contributed back from https://github.com/xinbenlv/zn-hermes-agent.

Closes #7015.
Co-authored-by: xinbenlv <zzn+pa@zzn.im>
2026-04-26 12:47:43 -07:00
Teknium
45bfcb9e71 test: update bare-agent helper for live-runtime attrs added by #16099
Background review fork now inherits session_id, credential_pool, and
status_callback from the parent (added in #16099 after this PR was
written). Extend the bare-agent helper so the regression test keeps
reaching the cleanup assertions instead of failing in the runtime
resolver.

Signed-off-by: Teknium <8425893+teknium1@users.noreply.github.com>
2026-04-26 12:45:39 -07:00
MRHwick
aa7b5acfcd pass attribution check 2026-04-26 12:45:39 -07:00
MRHwick
36e352afa7 preserve the original comment 2026-04-26 12:45:39 -07:00
MRHwick
2d86e97a7e fix(run_agent): shut down background review memory providers
Temporary background review agents can initialize Hindsight-backed memory clients, but close() alone skips provider teardown. Shut the memory provider down before closing so aiohttp sessions do not leak at process exit.

Made-with: Cursor
2026-04-26 12:45:39 -07:00
Teknium
edadeaf495 chore(release): map Satoshi-agi and kunlabs in AUTHOR_MAP 2026-04-26 12:35:16 -07:00
kunlabs
f9885130b4 fix(slack): download files in Slack Connect channels
Slack Connect channels return file objects with file_access="check_file_info"
and no url_private_download field (see
https://docs.slack.dev/reference/objects/file-object/#slack_connect_files).
These stub objects must be resolved via files.info before download can
proceed. Without this the agent silently skips attachments posted in
Slack Connect channels.

Call files.info on every file whose file_access is check_file_info,
replace the stub with the full file object, and let the existing
download path continue. Warn and skip on files.info failures.

Closes #11095.
2026-04-26 12:35:16 -07:00