Commit graph

2114 commits

Author SHA1 Message Date
Minseo-Choi
f716b876a8 fix(slack): edit status bubbles in place instead of posting new ones
Progress/status callbacks (context-pressure, compression retries,
model fallback) route through _send_or_update_status_coro, which
edits the previous bubble for the same status_key when the adapter
implements send_or_update_status — but only Telegram did. On Slack
every status event posted a fresh thread message, so a compression
retry loop spammed a dozen out-of-order bubbles into the thread
('Context too large 1/3... 2/3... 3/3', fallback switches, etc.).

Implement send_or_update_status on the Slack adapter following the
Telegram pattern (#30045): first call posts and caches the message ts
per (channel, thread, status_key); subsequent calls edit that message
via chat.update. Edit failure drops the cached ts and falls back to a
fresh send. Cache is FIFO-bounded.
2026-07-23 11:36:49 -07:00
Teknium
c132783ea0 test(slack): regression coverage for thread image/file context visibility
Covers cluster C1-images (#69185, #32315, #66136):
- _slack_file_marker unit tests: typed markers per mimetype family,
  hostile-filename sanitization (newlines/brackets can't fake context
  structure).
- _render_message_text appends markers; a caption-less image post no
  longer vanishes from thread context.
- Cold-start hydrate integration: prior-message images surface as
  markers in channel_context; the thread root's image is downloaded,
  delivered as media_urls, and upgrades message_type to PHOTO.
- Failure path: root-image download failure degrades to the marker,
  never blocks the turn.
- Bounds: root delivery capped at _THREAD_ROOT_IMAGE_MAX; non-image
  root attachments stay marker-only (no download).
- One-time delivery: active thread session skips the hydrate → no
  re-download/re-delivery on later turns.
- Composition: the trigger's own event files still ride alongside a
  delivered root image; Slack Connect stubs resolve via files.info;
  the collector never issues its own conversations.replies call.
- Delta refresh (#23918 path): images in new replies past the watermark
  surface as markers, with no root re-download.

A/B: 14 of 15 tests fail with the adapter fix reverted, all pass with
it applied.
2026-07-23 11:35:03 -07:00
Teknium
3ec5a06f4f test(slack): mark block-privacy fixture message as human-authored (client_msg_id)
#58478's caplog test predates main's unlabeled-bot users.info probe
(#69xxx wave-2 gating): events without client_msg_id now hit
_resolve_user_is_bot, which the fixture's mock client doesn't wire up
(AttributeError on _user_is_bot_cache). Real human-authored Slack
messages carry client_msg_id — add it to the fixture so the test
exercises the intended block-extraction path.
2026-07-23 11:34:11 -07:00
Teknium
44f6f8435b test(slack): behavioral log-noise/privacy suite + keep clarify choice text out of INFO logs
Follow-up hardening for the C13 log-noise cluster:

- plugins/platforms/slack/adapter.py: clarify button resolution logged
  the full chosen option text at INFO (choice=%r). Choice text is user
  content — log the choice INDEX at INFO and the (truncated, %.100r)
  text at DEBUG only. Widens #58478's principle: no message content
  above DEBUG level anywhere in the adapter.

- tests/gateway/test_slack_log_noise.py (new): behavioral suite pinning
  the cluster's invariants:
  * catch-all ack registered AFTER every named handler (registration
    order is bolt's dispatch priority — no shadowing);
  * catch-all fires for an unsubscribed event type (reaction_added),
    logs only a DEBUG line naming the type, and never logs content;
  * named handlers still dispatch (message → _handle_slack_message);
  * end-to-end inbound message run leaves NO message text or block
    content in any adapter log record (caplog at DEBUG);
  * #30185's event-arrival diagnostic is metadata-only;
  * clarify resolution: INFO carries index/user only, text is
    DEBUG-only (fails with the adapter fix reverted — A/B verified).

Content-leak audit of all 139 logger call sites in the adapter found
two above-DEBUG leaks: the clarify choice line (fixed here) and none
else carrying message text; remaining sites log error strings, URLs
via safe_url_for_log, ids, and counts. The block-extraction DEBUG
preview was already removed by #58478 (chars= length only).
2026-07-23 11:34:11 -07:00
ygd58
565ea19a49 test(slack): pin catch-all event matcher registration and non-shadowing
Regression test for the catch-all ack: a re.compile(r'.*') event matcher
must be registered (after every named handler, so it never shadows
message/app_mention/reaction/file routing) and must match unhandled
subscribed event types like member_joined_channel / channel_archive /
pin_added.

Salvaged from #64218 (test half only — its adapter-side catch-all is a
duplicate of #38847, which landed as the base commit of this cluster
with first-submitter credit). Fixes #6572.

Co-authored-by: shivasymbl <sdevinarayanan@asymbl.com>
2026-07-23 11:34:11 -07:00
wpeterr
390ddfd028 fix(slack): quiet Slack display defaults — no heartbeat/busy-ack breadcrumbs in channels
Slack posts are durable workspace messages, not an ephemeral terminal
status area. Default long_running_notifications and busy_ack_detail to
off for Slack so long-running agent work does not leave permanent
operational breadcrumbs like 'Working — 9 min — iteration 12/90' in
channels. Both remain opt-in per platform via
display.platforms.slack.*.

Also covers the platform-generic shutdown-notification mute path with a
regression test (gateway_restart_notification=false must suppress both
the active-session interruption notice and the home-channel copy).

Salvaged from #69028 (quiet-defaults half only). The PR's other half —
the channel_session_scope_channels session-scoping feature — is a new
config feature outside this log-noise cluster and overlaps the session
scoping territory reworked by merged wave-1/2 Slack session work; it is
deliberately not taken here.
2026-07-23 11:34:11 -07:00
luyifan
ece3dd1a4f fix(slack): avoid logging block text previews 2026-07-23 11:34:11 -07:00
nanckh
c1529e58da fix(gateway): quiet Slack missing_scope channel directory fallback
Treat Slack users.conversations missing_scope as an expected limited-scope app condition and fall back to session history without recurring warnings.

Add tests for not-ok and SlackApiError-like missing_scope responses.
2026-07-23 11:34:11 -07:00
haran2001
0eb5cb5e07 fix(slack): surface bot-event arrival and allow_bots interop diagnostics (#30091) 2026-07-23 11:34:11 -07:00
LeonSGP43
42534605b7 fix(slack): throttle channel directory warnings 2026-07-23 11:34:11 -07:00
Teknium
e027f43f70 fix(gateway): key Slack capability gate into the prompt pin; defer positive note to tool schemas
Follow-up to the #68627 cherry-pick (cluster C15 — Slack platform
capability-note accuracy; earliest report/fix: #6545 by @daikeren):

1. Session/prompt stability: the pinned session-context render
   (_pinned_session_context_prompt) is keyed by _ephemeral_change_key,
   whose contract requires every rendered input to appear in the key.
   The new _slack_tools_loaded() gate reads config + the live MCP
   registration map, so its state is now hashed into the key exactly
   like the existing Discord gate — a gate flip re-renders ONCE (a
   legitimate bust); within a session the note stays byte-stable for
   the life of the conversation (A/B: the new parity test fails with
   this key change reverted, passes with it).

2. Derived, non-overpromising positive note: rather than hardcoding a
   capability list that can drift stale again (the original bug class),
   the tools-present note tells the agent to consult the actual loaded
   Slack tool schemas for supported operations — the schemas ARE the
   source of truth, so the note cannot overclaim ops a given Slack
   toolset/MCP server doesn't expose (e.g. a read-only history server).

3. Tests: parity test proving a gate flip changes both render and key;
   byte-stability test proving three consecutive turns in one Slack
   session return the identical pinned object (sha256-equal); autouse
   fixture pins the new gate so key<->render parity is env-independent.
2026-07-23 11:32:24 -07:00
ygd58
96f21e8a54 fix(gateway): make Slack platform note capability-aware when slack tools present
Ports #63234 forward onto current main per teknium1's review.

gateway/session.py hard-coded the stale-API disclaimer for every Slack
session regardless of whether Slack tools were actually loaded. This
contradicted the system prompt when MCP or native slack tools were
present, causing the agent to refuse Slack API actions it could
actually perform (issue #6536).

Per review, the original predicate only checked the native 'slack'
toolset, missing Slack MCP servers (registered under mcp-<server> in
tools/mcp_tool.py) entirely. _slack_tools_loaded() now checks two
independent paths:

1. Native 'slack' toolset + SLACK_BOT_TOKEN (as before, but now calls
   _get_platform_tools() with include_default_mcp_servers=True instead
   of False, so a default-enabled MCP server also counts).
2. A connected MCP server that has ACTUALLY registered tools into the
   live registry (new tools.mcp_tool.get_registered_mcp_server_names()),
   whose name suggests Slack. This is session-scoped in the sense that
   matters here: MCP servers connect once per gateway process (not
   per-session), so checking the live per-server tool-registration map
   is the correct availability-filtered signal -- unlike the earlier
   get_all_tool_names() approach this replaces, which conflated ALL
   built-in tool names process-wide, this only inspects the small,
   purpose-built MCP server-name map.

Added a real regression test that registers a tool via the actual
tools.mcp_tool._track_mcp_tool_server() tracking function (not a mock
of the capability check) to verify a genuine Slack MCP server is
detected, plus a negative case for an unrelated MCP server.

5/5 Slack-specific tests pass; 126/126 in the full
tests/gateway/test_session.py file.
2026-07-23 11:32:24 -07:00
Teknium
24d7333eb1 test(gateway): cover msgraph_webhook port/host/secret bridging + contributor mapping for #57320
Follow-up to the salvaged PR #57320 commit: the PR bridged port/host/secret
for MSGRAPH_WEBHOOK but only tested webhook and api_server. Adds a test
covering the msgraph_webhook branch including extra-precedence, plus the
contributors/emails mapping for kjames2001.
2026-07-23 11:31:46 -07:00
James Huang
74db4bfe68 fix(gateway): bridge top-level port/host into extra for webhook and api_server
WebhookAdapter and ApiServerAdapter read port/host from config.extra, but
PlatformConfig.from_dict only populates extra from the 'extra:' sub-key in
the YAML platform section. Top-level keys like port and host are silently
ignored, causing the adapter to fall back to DEFAULT_PORT (8644).

This causes silent port conflicts in multi-profile setups: a profile that
configures 'platforms.webhook.port: 8649' still binds 8644, colliding with
the default profile's webhook on the same port.

Fix: extend the shared-key bridging loop in load_gateway_config() to bridge
top-level port/host/secret into extra for WEBHOOK, MSGRAPH_WEBHOOK, and
API_SERVER platforms, following the same pattern already used for
dm_policy, allow_from, gateway_restart_notification, and other keys.

The extra dict takes precedence: if port is already under 'extra:', the
top-level value does not clobber it.
2026-07-23 11:31:46 -07:00
Teknium
ada06ee7b0
test(gateway): loosen hygiene-timeout wall-clock bound to flake policy minimum (#70202)
0.15s missed by 1-8ms on busy CI shards twice today (runs 30025889952,
30026770278 — branches not touching this file). The assertion pins
'handler did not block on the timeout path' (blocking = seconds); 2.0s
keeps the contract per the loose-bounds flake policy.
2026-07-23 11:29:59 -07:00
davidgut1982
c7fd3eb377 fix(gateway): honor explicit api_server enabled:false under env key
_apply_env_overrides() force-set ``api_server.enabled = True`` whenever
API_SERVER_KEY (or API_SERVER_ENABLED) was present in the environment.

In multiplex mode, a secondary profile pins
``platforms.api_server.enabled: false`` in its config.yaml so that it
shares the default profile's API-server listener instead of binding its
own port. That profile still inherits the process-level env, including
API_SERVER_KEY, so the unconditional re-enable flipped api_server back on
and tripped the MultiplexConfigError check.

Honor an explicit disable, flagged by ``_enabled_explicit`` in the
platform's extra. Use ``extra.pop("_enabled_explicit", False)``: the
api_server branch is terminal (unlike the migrated plugin platforms, no
later registry pass re-enables api_server), so popping consumes the flag
in a single read and avoids the double-read hazard, while the final
per-platform cleanup remains a no-op.

Adds a regression test asserting that with API_SERVER_KEY set, a config
with api_server explicitly enabled:false + _enabled_explicit:true survives
_apply_env_overrides() as enabled=False (fails without the fix), while the
key is still wired through for the shared listener.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:21:06 -07:00
Teknium
0dbf639bc8
fix(windows): hidden-console daemons — extend the parent-console fix to every detached spawn path (#70205)
Extends the desktop backend's root-cause fix (aa2ae36c3f) to all remaining
console-less parent launch paths. The Windows console-flash class
(#54220/#56747) is governed by the PARENT's console: a DETACHED_PROCESS or
pythonw.exe daemon has no console, so every console-subsystem descendant
(git, gh, cmd, node, wmic, powershell) allocates its own visible conhost —
one flash per spawn, unreachable by any per-call-site CREATE_NO_WINDOW
sweep. Worse, MSDN specifies CREATE_NO_WINDOW is IGNORED when combined
with DETACHED_PROCESS, so the hide bit in the old detach bundle was dead.

Changes:
- _subprocess_compat: drop DETACHED_PROCESS from windows_detach_flags()
  and windows_detach_flags_without_breakaway(); the daemon now owns a
  single hidden console (CREATE_NO_WINDOW) that all descendants inherit.
- gateway_windows: _resolve_detached_python() returns the venv console
  python.exe (no pythonw/base-interpreter detour — the uv-shim flash
  premise only held while DETACHED_PROCESS was masking the hide bit);
  UAC handoff launches console python under SW_HIDE; cmd/vbs launchers
  render console python (vbs runs it window-style 0).
- gateway/run.py: restart watcher keeps sys.executable instead of
  swapping in GUI-subsystem pythonw.
- web_server: dashboard actions spawn sys.executable (already carries
  windows_detach_flags()).

Tests updated to pin the new invariants, including an explicit
DETACHED_PROCESS-must-stay-out regression guard.
2026-07-23 11:13:14 -07:00
Teknium
1d1b670cb5 fix(compression): reset blocked-overflow dedup on every compression path + noise-filter survival pins
Follow-up fixes for the #62625 salvage:

- Dedup-reset gap (sweeper review): when the block clears while the
  context is STILL over threshold, execution enters the compression
  branch — the PR's 'else' reset never ran, so the warning stayed
  suppressed forever after the first block. _clear_context_overflow_warn()
  now fires on every automatic compression path: turn-context preflight,
  conversation_loop pre-API gate, and the post-tool loop-compaction gate.
- should_compress_info on current main: main refactored should_compress
  into _automatic_compression_blocked()/_locally(); the tuple variant now
  derives its reason from the same in-memory state via
  _compression_block_reason(), keeping cooldown:<s>/ineffective shapes.
- ContextEngine.should_compress_info ABC default now actually returns
  (should_compress(tokens), None) — the PR's default had a docstring but
  no return (returned None, would crash tuple-unpacking call sites).
- Below-threshold guard: the turn-context persisted-cooldown branch and
  the conversation_loop pre-API cooldown branch no longer warn when the
  estimate is under threshold (should_compress_info returns a None
  reason; the preflight pre-check is not a threshold guarantee). The
  pre-API guard also honors compression.max_attempts instead of a
  hardcoded 3, and no longer fabricates a cooldown reason.
- Noise-filter survival (#69550 composition): warning text is now a
  template constant (CONTEXT_OVERFLOW_BLOCKED_WARNING_TEMPLATE) marked
  FAILURE-CLASS, pinned un-swallowed in VISIBLE_COMPRESSION_MESSAGES and
  in new tests that execute the real _TELEGRAM_NOISY_STATUS_RE +
  _prepare_gateway_status_message.
- Contributor mapping for stanislav@local -> sl4m3.
2026-07-23 08:43:21 -07:00
Mustafa
dc3e4e8428 fix(kanban): keep delegated results in worker turn
Dispatcher-spawned Kanban workers are finite one-shot processes, so detached delegation completions can outlive their only consumer. Mark that runtime as unable to deliver async completions and reuse the synchronous delegation fallback, returning required child results before the worker exits.\n\nAlso make unsupported-session notes runtime-generic and cover the delayed-child lifecycle regression.\n\nRefs #63169
2026-07-23 08:33:55 -07:00
Teknium
eb7be2edde fix(compress): classify unconfirmed lock-acquire failures and cover all manual-compress surfaces
Follow-up to the salvaged #57634 commits:

- agent/manual_compression_feedback.py: new describe_compression_lock_skip()
  — single source of truth for lock-skip wording. A descriptive holder
  string means another compressor CONFIRMED holds the lock ('already in
  progress (holder: ...)'); True/None means acquisition failed without a
  confirmed holder (hermes_state.try_acquire_compression_lock catches
  sqlite3.Error internally and returns False), so the message says
  'could not acquire ... the lock check failed' instead of falsely
  claiming a concurrent compression is running.
- cli.py, gateway/slash_commands.py, tui_gateway/server.py (all three
  in-process consumers: session.compress RPC, command.dispatch compress
  branch, slash.exec mirror) now route through the shared helper.
- tui_gateway/server.py command.dispatch compress branch: catch
  CompressionLockHeld explicitly — it previously fell into the generic
  'compress failed' error handler.
- Deferred-notify contract (#69324): lock-skip discards the pending
  context-engine notification (committed=False) in _compress_session_history
  and the CLI path before returning.
- tests: lock-skip wording pins per surface, VISIBLE_COMPRESSION_MESSAGES
  noise-filter carve-outs for both wordings, MagicMock signal opt-outs for
  sibling tests added on main after the original PR.
2026-07-23 08:19:14 -07:00
Ethan
e8000b42e7 fix: prevent stale lock-skip signal leaking between compress_context calls
Advisor review found a critical stale-signal leak: if auto-compress
sets _compression_skipped_due_to_lock during a lock-skip, a subsequent
successful manual /compress will see the stale signal, falsely report
'Compression already in progress', and discard the compression results.

Fix:
- compress_context clears _compression_skipped_due_to_lock = None at
  entry so each call's outcome alone determines the signal.
- Unified gateway 'holder: unknown' drift to match CLI/TUI pattern
  (omit holder clause when not a descriptive string).
- Added MagicMock opt-outs in 3 sibling test files broken by the new
  signal check (test_compress_here, test_compress_focus,
  test_compress_plugin_engine).
- Added stale-signal-leak invariant test proving the fix.
2026-07-23 08:19:14 -07:00
Ethan
eed6bb14bc fix(gateway): show lock-hold reason when /compress no-ops 2026-07-23 08:19:14 -07:00
Drexuxux
683059feb5 fix(api_server): fail closed when API_SERVER_KEY strength can't be verified
`_api_key_passes_startup_guard` refuses to start the API server on a weak
`API_SERVER_KEY`, and its own log says why:

    This endpoint dispatches terminal-capable agent work — a guessable key
    is remote code execution.

But the check is wrapped so that a failure to import it starts the server
anyway:

    try:
        from hermes_cli.auth import has_usable_secret
        if not has_usable_secret(self._api_key, min_length=16):
            ... return False
    except ImportError:
        pass
    return True

`hermes_cli.auth` imports httpx at module scope and pulls in a large slice of
the CLI, so an import failure is not hypothetical — a trimmed image, a partial
install, or a circular import during gateway startup all produce one. When it
happens the strength check silently disappears and only the presence check
above it remains, so a placeholder key passes.

Reproduced against the real guard with the import blocked:

    weak key, normal          : False
    weak key,   ImportError   : True    <-- starts on a 4-char key
    strong key, normal        : True

Fail closed instead: an unverifiable key does not get to expose the endpoint,
and the log names the actual problem so the operator can repair the install.
This is the posture tools/credential_files.py already takes — it refuses a
mount when its deny-list cannot be consulted rather than risking it. The catch
also widens from ImportError to Exception, so an AttributeError or an error
raised inside the check cannot reopen the same hole.

Both happy paths are untouched: a strong key still starts, a weak or missing
key is still refused with the existing messages.

Unrelated to #38803, which fixes the retry behaviour after this guard rejects
and assumes the guard ran.

tests/gateway/test_api_server.py: new TestApiKeyStartupGuardFailsClosed — a
weak key is refused when the check is unavailable, a strong key is refused too
(fail-closed), plus three controls pinning the unchanged normal paths. The two
fail-open tests fail on main; the three controls pass there. 222 passed in the
api_server suites; 1475 passed across every suite touching api_server, with
the same 8 pre-existing failures on clean main.
2026-07-23 07:34:26 -07:00
kshitij
ca9c30c7f0 fix(gateway): bound hygiene compression failures 2026-07-23 07:26:27 -07:00
mattshapsss
9d4cc12605 fix(photon): register Photon as a low-verbosity display tier
Photon (managed iMessage) shipped without a _PLATFORM_DEFAULTS entry, so it
inherited the noisy global ('all') display defaults and narrated tool
progress / heartbeats / busy-ack detail into a permanent-message iMessage
thread. Register it as TIER_LOW alongside BlueBubbles and Signal, plus a
regression test guarding the tier.

Salvaged from #50511 (Photon tier piece only).
2026-07-23 07:24:21 -07:00
wz-heng
91546b8337 fix: preserve named custom provider vision overrides 2026-07-23 17:57:33 +05:30
Teknium
b0358cf3c8 fix(slack): unify DM-resolution caches and bound wave-2 caches (C16 policy)
Post-rebase consolidation over the merged C7/C16/C10 work:
- _ensure_dm_conversation now records workspace ownership via
  _remember_channel_team and bounds _dm_conversation_cache (cap 5000)
- module-level _slack_dm_cache (C7 standalone path) bounded oldest-first
- _user_is_bot_cache (C10) bounded with _trim_oldest_dict_entries
- caption-mode contract tests updated for the C7+C8 merged media path
2026-07-22 21:58:13 -07:00
Teknium
994a405f2d fix(slack): resolve bare user targets to DMs across all adapter send paths
Widen the #19237 send_message fix to the live adapter: send, _upload_file,
send_multiple_images, send_image, send_video, send_document,
send_exec_approval, send_slash_confirm, and send_clarify now route bare
Slack user IDs (U.../W...) through a shared _ensure_dm_conversation helper
before calling chat.postMessage / files_upload_v2, which reject user IDs.

This closes the gap in #17261 where an attachment worked when replying in
a thread but failed when directed at a user DM, and extends the DM-open
fallback to clarify/approval Block Kit prompts so gated actions can reach
a user directly.

Resolution uses the workspace-scoped client (multi-workspace installs open
the DM with the right bot token), caches per (team, user), and records the
opened D... channel in the channel→team map. On failure the original
target passes through so the downstream API call surfaces the real Slack
error.

Fixes #17261
Refs #19236
2026-07-22 21:58:13 -07:00
Teknium
4ab4894f44 fix(gateway): make post-stream media delivery explicit-only
The post-stream helper (_deliver_media_from_response) rescanned the
already-streamed response and promoted bare local filesystem paths into
real uploads via extract_local_files. Since the visible reply was already
streamed verbatim, any bare path there is either text the user has seen
or stale inspected/tool content — not an attachment request. On Slack this
uploaded images from stale inspected content after otherwise clean replies.

Post-stream delivery now honors only explicit MEDIA: directives. The
non-streaming path in gateway/platforms/base.py keeps its bare-path
auto-detect, because that path controls the visible text and strips the
path from the reply when it attaches — auto-attach is intentional there.

Regression tests: bare image/document paths in a streamed reply produce
no upload; explicit MEDIA: tags still deliver.

Fixes #20834
2026-07-22 21:58:13 -07:00
dsad
c83a196402 fix(slack): gate message files before metadata fetch 2026-07-22 21:58:13 -07:00
dsad
f54e8706f7 fix(platforms): block image upload redirects to private URLs 2026-07-22 21:58:13 -07:00
Rob Zolkos
40351d0923 fix(slack): resolve file events with cached workspace team 2026-07-22 21:58:13 -07:00
brooklyn!
7d28e84e72
fix(gateway): deliver assistant prose before the clarify poll (#69775)
* fix(gateway): deliver assistant prose before clarify poll

The clarify poll is sent on a separate, agent-thread-blocking path while
buffered assistant prose (interim commentary / streamed deltas) sits in
the GatewayStreamConsumer queue, drained asynchronously. The poll won the
race, so the question rendered ABOVE its own explanation, and a redundant
'clarify: ...' tool-progress bubble wedged between them.

- Add GatewayStreamConsumer.flush_pending_sync(): a synchronous flush
  barrier (_FLUSH sentinel + threading.Event) that blocks the agent
  thread until everything queued before it is finalized and delivered.
- Call it in the gateway clarify callback before send_clarify, so prose
  always lands before the poll. Best-effort with a 3s timeout.
- Suppress the redundant clarify tool-progress bubble (the poll already
  shows the question + options).

Tests: 3 new ordering/timeout cases in test_stream_consumer.py.
(cherry picked from commit 9a6e27badb)

* chore(contributors): map matvey.sakhnenko03@icloud.com -> sakhnenkoff

Attribution mapping for the salvaged #54328 commit (Cluster C).

---------

Co-authored-by: Matvii Sakhnenko <matvey.sakhnenko03@icloud.com>
2026-07-22 23:25:35 -05:00
LeonSGP43
0bbf679e4c test(slack): add peer-agent smoke target 2026-07-22 21:14:44 -07:00
Teknium
b416907538 feat(slack): require_mention_channels per-channel force-mention override
Port of the Slack half of #13855 (by @kshitijk4poor), reimplemented against
the plugin adapter (the original PR targets the deleted
gateway/platforms/slack.py and six other legacy adapters).

Channels listed in slack.require_mention_channels (config.yaml) or
SLACK_REQUIRE_MENTION_CHANNELS ALWAYS require an explicit @mention, even
when require_mention is false globally or the channel is in
free_response_channels — the opposite direction of free_response_channels.
Instead of duplicating the PR's inline reply-to-bot-thread/mentioned-thread/
session checks, the forced channel falls through to the SAME decision chain
as normal mention gating, so all five wake checks in
_should_wake_on_unmentioned_message keep applying (single decision path).

Credit: adapted from #13855 by @kshitijk4poor (Slack half only; the other
platform halves target deleted legacy adapters and are out of scope for
this cluster).
2026-07-22 21:14:44 -07:00
YLChen-007
38863322a4 Harden gateway mass mention handling 2026-07-22 21:14:44 -07:00
Ben Phillips
286ec6afb3 fix(slack): count pipe-form bot mentions as mentioned in ignore gate
The ignore_other_user_mentions gate relied solely on is_mentioned, which
only recognises exact <@UID> markup, so a message mentioning the bot in
pipe form (<@UID|name>) alongside a leading other-user mention was
wrongly suppressed. The gate now also scans for the bot's own mention in
either markup before ignoring a message.

Claude-Session: https://claude.ai/code/session_01TKsNdptNdo9CqT2u7JMdkH
2026-07-22 21:14:44 -07:00
Ben Phillips
7f9cab15d8 feat(slack): add ignore_other_user_mentions option
Once the bot is @mentioned in a Slack thread it auto-follows and replies
to every later message in that thread, including ones a human addresses to
another human (e.g. "@rasha check this out"). The bot butts in.

Add slack.ignore_other_user_mentions (env SLACK_IGNORE_OTHER_USER_MENTIONS,
default off). When on, a channel/thread message whose first token @mentions
someone other than the bot is treated as addressed to that person and the
bot stays silent unless it is also mentioned. This is Slack parity for the
Discord option of the same name (#33501), adapted to Slack's thread model:
the trigger is a leading mention ("addressed to"), so a message that merely
references another user mid-sentence still reaches the bot.

The gate sits ahead of the free-response / require_mention ladder so it also
overrides the mentioned-thread auto-follow. DMs are never filtered.
2026-07-22 21:14:44 -07:00
Seth Miller
49497bcddb fix(slack): handle edited-in bot mentions 2026-07-22 21:14:44 -07:00
Hafiz Ahmad Ashfaq
69e15d630e fix(slack): filter unlabeled app and bot authored events
(cherry picked from commit e2e11dab51)
2026-07-22 21:14:44 -07:00
Teknium
2168df37b8 fix(slack): harden parent-text wake check against None + restore fixture parity
Follow-up rework on the #51627 cherry-pick:
- Guard the 5th wake check (parent-mentioned-bot, #24848) against a None
  parent_text — _fetch_thread_parent_text is typed to return str but tests
  (and defensive callers) can surface None; 'in None' raised TypeError.
- Drop the PR's fixture-level _fetch_thread_parent_text/_fetch_thread_context
  AsyncMocks from TestThreadReplyHandling/TestAssistantThreadLifecycle: main's
  #24848 tests exercise the real parent-text path via conversations_replies
  side effects, and the blanket mocks broke them.
- _resolve_user_is_bot reworked to the workspace-scoped (team_id, user_id)
  cache key introduced by the multi-workspace name cache on main.
2026-07-22 21:14:44 -07:00
Alan Alwakeel
3f08201bac Fix Slack peer bot status routing loops 2026-07-22 21:14:44 -07:00
Teknium
533e541237 fix(slack): bound remaining per-message/per-user tracking structures with oldest-first eviction
Widening pass over the whole adapter following the cluster-C16 audit
(#51019, #51097, #23676, #23375): every in-memory structure that
accumulates per-message, per-user, or per-thread state is now bounded,
and every eviction is oldest-first — never arbitrary set-iteration
order, which is the #51019 failure mode (bot silently going quiet on
the most ACTIVE thread because set.pop-order eviction removed it).

Newly bounded:
- _approval_resolved / _clarify_resolved (caps 1000): unclicked
  approval/clarify prompts leaked their double-click-guard entries
  forever; oldest-insertion eviction via _trim_oldest_dict_entries.
- _reacting_message_ids (cap 5000): reaction lifecycle entries leaked
  when an exception fired between add and finalize; oldest-ts eviction.
- _active_status_threads (cap 1000): statuses abandoned by error paths
  accumulated; oldest-thread-ts eviction so the newest live status is
  never cleared.
- _channel_team (cap 10000): grew with every DM channel the bot ever
  saw (DM channel IDs are per-user). All four write sites now route
  through _remember_channel_team; eviction is safe because entries are
  re-learned from the next event and _get_client falls back to the
  primary client.
- _slash_command_contexts (cap 1000): TTL cleanup only ran on lookup,
  so contexts whose ephemeral replies never happened accumulated;
  overflow purges expired entries first, then oldest-stash-first.

Converted from arbitrary set-order eviction to oldest-first:
- _titled_assistant_threads: keys are (team, channel, thread_ts) —
  now evicts oldest thread first via _discard_oldest_by_thread_ts.
- _thread_rehydration_checked: keys are team:channel:thread_ts[:user] —
  arbitrary eviction here would re-run an ACTIVE thread's restart
  rehydration check and re-inject the missed-delta context; now evicts
  oldest thread first.
- _reacting_message_ids uses #51097's _discard_oldest_slack_timestamps.

Deliberately NOT bounded (naturally tiny, per-workspace):
_team_clients, _team_bot_user_ids, _team_bot_names (one entry per
installed workspace), _assistant_threads / _agent_view_contexts /
_bot_message_ts / _mentioned_threads / _user_name_cache /
_thread_context_cache (already bounded), _dedup (MessageDeduplicator
has max_size + TTL internally).

New helpers: _trim_oldest_dict_entries (dicts preserve insertion
order, so oldest-first is exact) and _discard_oldest_by_thread_ts
(chronological sort on the embedded Slack ts for keyed sets).

Tests: caps hold under churn, eviction removes OLDEST not arbitrary
entries, newest/active entries survive eviction pressure (regression
shape for #51019), plus end-to-end paths through _resolve_user_name
and _handle_slash_command.

Part of the C16 cache-bounds consolidation with #51097 (markoub),
#23676 and #23375 (EloquentBrush). Fixes #51019.
2026-07-22 20:59:12 -07:00
EloquentBrush
d42b295792 fix(slack): bound _thread_context_cache with eviction on overflow
_fetch_thread_context() caches per-thread Slack history under a
60-second TTL, but expired entries are never removed. On the current
adapter this is worse than when first reported: each entry now also
retains the raw conversations.replies payloads (messages) for
watermark re-formatting, so a busy multi-channel workspace accumulates
full message lists forever. _bot_message_ts, _mentioned_threads, and
_assistant_threads all enforce a MAX constant with active eviction in
the same __init__; _thread_context_cache had the TTL declared but no
matching eviction path.

Fix: add _THREAD_CACHE_MAX = 2500 and purge expired entries
(fetched_at older than _THREAD_CACHE_TTL) whenever a new write pushes
the cache past the limit. TTL-based eviction is used instead of LRU
because fresh entries are still needed; evicting them would
immediately re-trigger a rate-limited conversations.replies call.

Adds two unit tests: one verifying stale entries are purged on
overflow, one verifying fresh entries survive.

Reapplied from #23375 onto the current adapter (moved to
plugins/platforms/slack/adapter.py; cache entries now carry raw
message payloads and parent_user_id).
2026-07-22 20:59:12 -07:00
markoub
4fb1796331 fix(slack): evict oldest tracked thread timestamps 2026-07-22 20:59:12 -07:00
Ben Barclay
d8cd7ac2e7
fix(relay): declare explicit relevance policy when require_mention is configured false (#69816)
Companion to gateway-gateway#160 (connector absent-row default flips to
mention-gated / requireAddress=true).

relay_relevance_policy() previously returned None whenever the projected
policy was all-falsy, on the premise that 'the connector's quiet default
already matches'. Once the connector defaults requireAddress to true,
that premise inverts for one case: an agent EXPLICITLY configured with
require_mention: false would stay silent and get mention-gated anyway,
with no way out.

The nothing-to-declare check now keys on 'require_mention is unset'
rather than 'require_mention is falsy': an explicit false IS a
configured knob and is declared to the connector; a genuinely
unconfigured platform still declares nothing and inherits the
connector's default. No wire/contract change.
2026-07-23 13:57:57 +10:00
Esther
c7b9dfa961 fix(slack): resolve user IDs to DM channels in standalone cron delivery
Cron jobs targeting a Slack user (deliver=slack:U…) failed with
channel_not_found: chat.postMessage and files_upload_v2 require a
conversation ID, and a DM must be opened first via conversations.open
to obtain a D… ID. The tool-level resolution in send_message() does not
cover cron's direct _send_to_platform → standalone_sender_fn path.

Add _resolve_slack_user_dm to the Slack plugin: resolves U…/W… targets
via conversations.open (proxy-aware, cached per token+user so repeated
cron fires don't re-open the DM), wired into _standalone_send ahead of
both the text and media delivery branches so every standalone entry
point benefits.

Adapted from #17444 by @Esther-Zhu023 — the original patched the legacy
_send_slack helper in tools/send_message_tool.py, which has since moved
into the plugin as _standalone_send (#41112).

Closes #17444
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-22 20:57:25 -07:00
kandotrun
4f726ed467 fix(slack): preserve media in standalone cron delivery 2026-07-22 20:57:25 -07:00
Teknium
700c8bfc5c fix(slack): fall back to chat.postEphemeral, never public channel, for slash replies
Per review (Victor): response_url failure does not mean ephemeral delivery
is impossible — chat.postEphemeral is an independent API path that keeps
the reply private. Public-channel fallback removed entirely; when both
ephemeral paths fail the reply is dropped with a logged error rather than
leaked to the channel. No config knob needed.
2026-07-22 20:55:51 -07:00
Teknium
1593bb82ac test(slack): adapt salvaged tests to current main
- test_thread_command_skips_context_prefix: post-#69320 thread context IS
  fetched on first thread entry but rides channel_context; assert the
  command token stays at char zero and the backfill is preserved, instead
  of asserting the fetch never happens.
- test_slack_send_retry.py: main's _get_client() now takes team_id;
  update the lambda stubs.
2026-07-22 20:55:51 -07:00