Commit graph

17460 commits

Author SHA1 Message Date
Alex Fournier
06ed41aa1f fix(relay): preserve managed cancellation signals
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:21:37 -07:00
Alex Fournier
fb1e417367 Merge remote-tracking branch 'origin/main' into merge/relay-metrics-upstream-20260723 2026-07-23 14:10:49 -07:00
Alex Fournier
ad6fb26681 fix(relay): fence bypassed stream chunks
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:09:33 -07:00
Alex Fournier
5e1b68ce16 test(telemetry): mock profile consent source
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:06:33 -07:00
Alex Fournier
d2e179c54f test(lifecycle): document finalize coverage owner
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:05:36 -07:00
Alex Fournier
6fa96916a8 docs(relay): disclose managed data boundary
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:05:11 -07:00
Alex Fournier
43d994986e fix(telemetry): keep consent profile-owned
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:04:07 -07:00
Alex Fournier
bba63d8786 fix(telemetry): prune expired local metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:01:41 -07:00
Alex Fournier
e1a6becf86 docs(telemetry): disclose local profile identity
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:58:22 -07:00
Alex Fournier
31387a4621 test(tools): verify hook ownership behaviorally
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:57:30 -07:00
Alex Fournier
4788994bd2 fix(relay): preserve managed callback context
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:53:59 -07:00
Alex Fournier
e9b5e4c8f4 fix(relay): protect turn initialization cleanup
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:48:52 -07:00
Alex Fournier
937fffcfec fix(smoke): resolve Hermes across environments
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:45:40 -07:00
Alex Fournier
1500ce163c fix(relay): normalize rewritten LLM responses
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:44:27 -07:00
Alex Fournier
a54e52aeb1 fix(tools): preserve sequential middleware trace
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:43:27 -07:00
Alex Fournier
0be07b0553 fix(middleware): hide Relay control flag
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:41:47 -07:00
Alex Fournier
2378bd4e4c fix(tools): prevent duplicate managed dispatch
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:40:44 -07:00
Alex Fournier
ea171c947a perf(relay): bypass inactive tool interception
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:38:49 -07:00
Prathamesh Chaudhari
4c9628eab5
fix(anthropic): coerce empty/whitespace-only text blocks on the request path (#69512) (#69517)
An assistant message with an empty or whitespace-only text content block —
produced by context compression or certain tool-call flows — is rejected by
the Anthropic Messages API with HTTP 400 "text content blocks must contain
non-whitespace text". Because the blank block is stored in session history and
replayed verbatim every turn, the session is permanently wedged behind the same
400.

The Bedrock adapter already guards this via _safe_text() (#9486); the native
Anthropic path never got the same treatment. _sanitize_replay_block() rebuilt
text blocks with the raw stored text, and the _convert_assistant_message()
guard only caught a fully empty block list, not a list still containing a
whitespace-only text block.

Add a _safe_text() helper mirroring the Bedrock one and apply it at both points:
the ordered-blocks replay path and a final in-place walk of the converted
content list. Both are self-healing — sessions that stored blank blocks recover
on the next API call. Only text blocks are coerced; thinking/tool_use/image
blocks are untouched.

Fixes #69512
2026-07-23 16:36:37 -04:00
Alex Fournier
36185bf2e2 feat(telemetry): expose shared metrics setup
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:36:00 -07:00
Alex Fournier
591ba267d2 fix(relay): release session lock before callbacks
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:33:41 -07:00
Alex Fournier
e862099dff fix(relay): merge Anthropic stream usage
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:32:44 -07:00
Alex Fournier
c789bdd38a fix(relay): close managed provider streams
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:32:03 -07:00
Alex Fournier
f370af6816 fix(codex): stop draining interrupted streams
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:26:49 -07:00
Alex Fournier
9dfe690125 fix(relay): preserve provider request extensions
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:24:19 -07:00
hermes-seaeye[bot]
053f162f94
fmt(js): npm run fix on merge (#70291)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 20:23:02 +00:00
Alex Fournier
a6fbff1d7e fix(relay): retain active child sessions
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:22:50 -07:00
Alex Fournier
398a8ca580 fix(tools): serialize concurrent approvals
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:19:03 -07:00
Alex Fournier
c16f86dc4c fix(relay): serialize rewritten tool results
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:13:58 -07:00
SHL0MS
53bdcacf17
fix(desktop): stop assistant reply rendering twice after a tool-call turn (#70232)
The renderer showed two assistant bubbles for one turn — a partial
streamed copy plus the clean final copy (#63679). A reload fixed it, so
the persisted transcript was correct; this was live reconciliation.

Root cause is in completeAssistantMessage (use-message-stream/index.ts).
message.interim fires for BOTH verify-on-stop candidates AND ordinary
tool-call turns (tui_gateway _load_interim_assistant_messages), and the
interim seal clears streamId + sets interimBoundaryPending. So at
message.complete the streamId fast-path is skipped and it enters the
fallback. There the settle-onto-interim branch was gated on
responsePreviewed — true ONLY for verify-on-stop. A normal tool-call
turn whose final text matched its sealed interim satisfied neither
settle branch and fell through to append a brand-new bubble. Two rows,
distinct ids, id-based dedup cannot collapse them → renders twice.

Fix: settle onto the sealed interim whenever the final CONTINUES it —
final == interim, final starts with interim (streamed + trailing delta),
or interim starts with final (streaming dropped characters). This is
gated on existing.interim so it only ever collapses a genuine sealed
interim, and a genuinely DIFFERENT final still appends as its own
bubble. responsePreviewed is retained as an OR so the verify-on-stop
continuation-budget case (final text rewritten, no shared prefix) still
settles as before.

The prior test that asserted a non-previewed identical interim+final
produced TWO bubbles was encoding the bug; rewritten to assert one.
Added coverage: prefix-extended non-previewed final collapses; a
genuinely different final still appends (no over-collapse).

Community analysis on the issue (seedSeenBubbleKeys / kind-guard) was
against a pre-refactor v0.7.0 bundle — those symbols no longer exist;
this is the current-code root cause.

Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
2026-07-23 16:13:01 -04:00
Alex Fournier
1c6a96032c fix(relay): preserve successful managed results
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:12:50 -07:00
Alex Fournier
1ffeaf226c test(relay): skip native suites without binding
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:04:12 -07:00
ethernet
75afaf46da
test(gateway): use valid API server key in env override test (#70274)
Keep the explicit-disable regression test on the usable-key path after
the API server loader began rejecting weak API_SERVER_KEY values.
2026-07-23 19:56:28 +00:00
Teknium
c4f5a45d5d fix(gateway): tolerate bare GatewayRunner instances in the ignored-channel guard
Gateway tests construct GatewayRunner via object.__new__ without __init__;
the new #51899 guard accessed self.config directly and crashed those
runners (AttributeError). Use getattr with None default — the guard
no-ops without config, matching the documented object.__new__ pitfall.
2026-07-23 12:01:24 -07:00
Teknium
66513768c1 chore: map contributor emails for mwbrooks and replygirl
Note: #51899's commit was authored as a placeholder identity
(hermes-agent@example.invalid); it was re-stamped to @byshubham's
numeric noreply on this branch, so no mapping file is needed. Other
salvaged commits use GitHub noreply addresses.
2026-07-23 12:01:24 -07:00
replygirl
d9fe008db8 fix(slack): prefer live send adapter and try multi-workspace tokens individually
Two related Slack delivery fixes for send_message text sends:

- Route Slack text delivery through _send_via_adapter so the live
  in-process gateway adapter (multi-workspace aware, channel→client
  mapping, adapter-side gates) is preferred, with the plugin's
  _standalone_send as the out-of-process fallback — matching how the
  media path already behaves.
- _standalone_send: SLACK_BOT_TOKEN can be a comma-separated list in
  multi-workspace installs and slack_tokens.json carries OAuth
  per-workspace tokens; the standalone Web-API path used to send the
  literal comma-joined string, which Slack rejects as invalid_auth.
  Try each token individually, retrying on token-scoped errors
  (invalid_auth / not_in_channel / channel_not_found …) and stopping on
  terminal ones. User-DM resolution (U…/W… targets) also tries each
  token.

Adapted from #47547 by @replygirl — the original patched the legacy
tools/send_message_tool.py::_send_slack helper, which moved to the
Slack plugin's _standalone_send in #41112.

Salvaged from #47547
2026-07-23 12:01:24 -07:00
dirtyren
8685fea0ce fix(slack): resolve channel IDs to human-readable names
Previously, Slack sessions and the channel directory stored raw channel
IDs (e.g. C0ATFHY907L) as chat_name, making it impossible for operators
to identify channels in send_message listings or session data.

Changes:
- Add _channel_name_cache and _resolve_channel_name() to SlackAdapter
  that calls conversations.info (channels) or users.info (DMs) with
  in-memory caching to avoid repeated API calls
- Use resolved names in _handle_slack_message() build_source() calls
- Use cached names in _seed_assistant_thread_session()
- Enrich session-sourced entries in channel_directory._build_slack()
  by cross-referencing API results and falling back to conversations.info
  + users.info for DMs and private channels not in the bot's scope

Fixes: channel directory and session origins now show readable names
like 'general' or 'John Doe' instead of 'C0xxx' / 'D0xxx'.
2026-07-23 12:01:24 -07:00
Teknium
da131aef3a feat(slack): bridge slack.ignored_channels through the YAML→env config path
Follow-up to the #51899 pick, folding in the config-bridge half of the
competing PR #46925 (@bhanusharma, earliest submitter for the
ignored-channel gate):

- _apply_yaml_config: translate config.yaml slack.ignored_channels into
  SLACK_IGNORED_CHANNELS (list or CSV), env-var-wins like every other
  bridged Slack key.
- SlackAdapter._slack_ignored_channels / gateway.run's
  _slack_ignored_channels_from_gateway_config: fall back to the
  SLACK_IGNORED_CHANNELS env var when PlatformConfig.extra carries no
  value, so top-level slack: blocks (which flow through the env bridge,
  not extra) are honored at both the adapter and runner gates.
- conftest: force-clear SLACK_ALLOWED_CHANNELS / SLACK_IGNORED_CHANNELS /
  SLACK_DISABLE_DMS between tests (config-loader side-effect leak class).
- Tests: env-bridge translation + precedence in test_config.py, env
  fallback + extra-wins in test_slack_runner_ignored_channels.py.

Credit: #46925 by @bhanusharma proposed the same gate with the YAML→env
bridge; #51899 (picked as the base) carries the wider outbound/runner
coverage. Closes #46925 as consolidated here with first-submitter credit.
2026-07-23 12:01:24 -07:00
2001Y
0a5d8a16fc feat(slack): support long app descriptions in the manifest generator
Add --long-description / --long-description-file to `hermes slack
manifest` so the generated app manifest can carry Slack's
display_information.long_description (175–4,000 characters), with
validation of the length bounds, mutual-exclusion with --slashes-only,
and UTF-8 file input. Also propagate the manifest command's exit status
through cmd_slack so validation failures reach the shell.

Squash of the two commits from PR #65256 — one commit per contributor
on this salvage branch.

Salvaged from #65256
2026-07-23 12:01:24 -07:00
AhmetArif0
805c22c836 fix(streaming): add Slack streaming=false default to match Discord
PR #37303 added per-platform streaming defaults and the commit message
explicitly called out "Discord/Slack/etc. only have edit-based streaming
(repeated editMessage), which flickers and is noticeably jankier" — but
only discord.streaming=false was shipped. Slack uses the same edit-based
streaming mechanism and has the same flicker problem, yet it was left to
follow the global switch (default true when streaming is enabled).

Add "slack": {"streaming": False} to DEFAULT_CONFIG["display"]["platforms"]
alongside the Discord default. The same deep-merge semantics apply: a user
who explicitly sets display.platforms.slack.streaming: true keeps their
value unchanged. The dashboard schema gains a slack.streaming toggle
automatically since it is generated from DEFAULT_CONFIG.

Update test_per_platform_streaming_defaults.py to cover slack in all
existing assertions and rename the resolver test to reflect both platforms.
2026-07-23 12:01:24 -07:00
Michael Brooks
2946805299 feat(slack): set HermesAgent User-Agent on slack-bolt client
Hermes already identifies itself on outbound calls to model providers
and monitoring endpoints — see hermes_cli/models.py and
plugins/model-providers/gmi/__init__.py
("Attribution so GMI can identify traffic from Hermes Agent"). The
Slack adapter is the one outbound surface that doesn't carry the
same identifier, so HermesAgent-driven Slack traffic is
indistinguishable from any other Bolt-Python app at the Slack
platform layer.

This sets `user_agent_prefix=f"HermesAgent/{_HERMES_VERSION}"` on the
AsyncWebClient instances constructed in gateway/platforms/slack.py
and threads them through AsyncApp via its `client` kwarg. Both
kwargs are first-class in slack-sdk and slack-bolt; no new
dependencies. Resulting header looks like:

    HermesAgent/<version> Python/3.x slackclient/3.x ...

No behavioral change for users — the Slack API ignores User-Agent
semantically; it lands in logs and analytics. Reversible.

Tests in tests/gateway/test_slack.py:
- TestSlackUserAgent pins the prefix shape and runs connect()
  end-to-end (multi-token config) to assert every AsyncWebClient
  carries the prefix and AsyncApp receives the pre-built client.
- TestSlackProxyBehavior fakes updated to tolerate the new kwargs
  via **_kwargs so they don't break on future passthroughs.
2026-07-23 12:01:24 -07:00
annguyenNous
c5b62fdbae fix(gateway): guard chained .get() against None intermediate values
.get("key", {}) only applies the default when the key is ABSENT.
When the key exists with value None (null in JSON), .get() returns
None and the subsequent .get() raises AttributeError.

Fix: replace .get("key", {}).get(...) with (.get("key") or {}).get(...)
which handles both missing keys AND None values.

8 instances across 6 files:
- gateway/run.py: tool_call function name check
- acp_adapter/server.py: tool name/description extraction
- gateway/platforms/qqbot/onboard.py: API response task_id
- gateway/platforms/yuanbao.py: message content parsing (x2)
- gateway/platforms/slack.py: block text extraction (x2)
- tui_gateway/server.py: error message extraction
2026-07-23 12:01:24 -07:00
David Metcalfe
241bc112e8 fix(platforms): clear home channel when setup prompt left blank
Blank (or whitespace-only) answers to the home-channel prompt in the
interactive setup wizards previously left any previously saved
*_HOME_CHANNEL / *_HOME_ROOM env value in place, so operators could not
clear a stale home channel by re-running setup. Strip the prompt input
and call remove_env_value() on blank answers across the Discord, Slack,
Feishu, Matrix, Mattermost, WeCom and WhatsApp plugin setup wizards,
with per-adapter wizard tests covering set/clear/whitespace flows.

Squash of the three commits from PR #58421 (setup-wizard fix, matrix/
wecom extension, and 6-adapter test coverage) — one commit per
contributor on this salvage branch.

Fixes #12423
Salvaged from #58421
2026-07-23 12:01:24 -07:00
vexclawx31
ee62aab1a7 fix(slack): honor disable_dms setting 2026-07-23 12:01:24 -07:00
byshubham
f8f5ce7da5 fix(slack): honor ignored channels before gateway dispatch 2026-07-23 12:01:24 -07:00
Teknium
543941f709 fix(slack): tolerate bare adapter instances in _remember_channel_team ambiguity map
The C13 log-noise test (merged while this branch was in flight) builds a
bare SlackAdapter without __init__; the new _channel_teams ambiguity map
crashed it. getattr-guard, same pattern as the sibling defensive inits.
2026-07-23 12:00:34 -07:00
Teknium
5ee1c426de chore: map contributor emails for C9 salvage (jordanhubbard, trac3r00, benjamin2026-dot) 2026-07-23 12:00:34 -07:00
Teknium
9439f11717 test(slack): pin download-token workspace routing (#59742)
Five regression tests for _resolve_download_token and the download
helpers: explicit team wins, URL-embedded team id routes to the owning
workspace, unknown/no-match fall back to the primary token, and an
end-to-end _download_slack_file_bytes call asserts the Authorization
header carries the owning workspace's token.

A/B: all five fail with the fix commit reverted, pass with it applied.
2026-07-23 12:00:34 -07:00
benjamin2026-dot
ca8aee87e8 fix(slack): route file downloads to the owning workspace via URL-embedded team id
Salvaged from #59742 (downloads half only). Main already resolves the
event-level team id from the channel→workspace cache before downloads
(the file-EVENT half was covered by #30456), but when neither the event
nor the cache knows the workspace, both download helpers silently fell
back to the PRIMARY workspace token — Slack then returns an HTML login
page instead of file bytes for any non-primary workspace file.

Slack private file URLs embed the owning workspace id
(files-pri/<TEAM_ID>-<FILE_ID>/...), so _resolve_download_token now
prefers: explicit team_id -> URL-embedded team id -> primary token.
Both _download_slack_file and _download_slack_file_bytes route through
it.

Deferred from #59742 (out of this correctness cluster's scope): the
channel→team disk persistence, the pre-send conversations.info probe
loop, and the thread-file ingestion feature.

Reapplied from #59742 by @benjamin2026-dot onto the current adapter
(original targeted the pre-#30456 download sites).
2026-07-23 12:00:34 -07:00
Teknium
c1c991ae9e fix(gateway): unify legacy-Slack-key recovery on the claim-once path
Conflict-resolution follow-up composing #68925 (Bob) with the already-
applied #20583/#66398 (jordanhubbard) recovery design:

- #68925's caller-level second _query_recoverable_session pass (via
  lookup_session_key=) referenced a variable that no longer exists —
  the legacy exact-key fallback now lives INSIDE
  _query_recoverable_session, which also claims the legacy key once per
  process and rewrites the peer row to the scoped key. Drop the dead
  caller-level pass.
- Keep #68925's _recovered_row_matches_source_scope origin guard wired
  into both recovery paths: a scoped channel lookup refuses rows whose
  recorded origin names another workspace (or no workspace at all).
- Routing-index migration adoption policy documented at the site:
  origin names a workspace -> exact match only; scope-less DM -> first
  workspace claims once; scope-less channel -> refuse.
2026-07-23 12:00:34 -07:00