Skip to content

feat: multi-workspace Socket Mode, Signal reactions/editing, budget reset, Ctrl+D fix - #13837

Closed
jordanhubbard wants to merge 7 commits into
NousResearch:mainfrom
jordanhubbard:feature/consolidated-upstream
Closed

jordanhubbard wants to merge 7 commits into
NousResearch:mainfrom
jordanhubbard:feature/consolidated-upstream

Conversation

@jordanhubbard

Copy link
Copy Markdown
Contributor

Summary

This PR consolidates four independent improvements, all rebased cleanly onto current upstream/main:

  1. Multi-workspace Socket Mode for Slack (gateway/platforms/slack.py)
  2. Signal adapter: reactions + message editing (gateway/platforms/signal.py)
  3. Agent iteration budget reset after context compression (run_agent.py, tools/delegate_tool.py)
  4. Ctrl+D: delete char under cursor (readline semantics) (cli.py)

1. Multi-workspace Socket Mode for Slack

Supersedes PR #6686 (rebased and conflict-free).

Upstream already merged send-side multi-workspace via OAuth token file (#3903). This adds receive-side multi-workspace: independent AsyncApp + AsyncSocketModeHandler per workspace so a single agent handles events from N workspaces simultaneously.

Changes:

  • _load_accounts() reads ~/.hermes/slack_accounts.json; falls back to env vars (single-workspace backward compat)
  • _register_app_handlers(app) extracted so handlers are registered identically per account
  • connect() iterates accounts, creates independent App + Handler + Task per account
  • self._handlers / _apps / _socket_mode_tasks are dicts keyed by account name
  • Graceful token degradation: auth_test() failures skip that account (not abort)
  • Persisted channel→team routing via slack_channel_teams.json (survives restarts)
  • Metadata-aware _get_client(chat_id, metadata) uses team_id from metadata before routing table
  • Per-workspace user_token (xoxp-) for user-level API calls
  • All upstream changes preserved: smart reaction guard, mpim-as-DM, mrkdwn=True, SSRF protection, per-thread sessions, rate-limit retry

2. Signal adapter: reactions + message editing

  • edit_message(chat_id, message_id, new_text) — uses editTimestamp RPC parameter
  • _send_reaction(chat_id, message_id, emoji) — emoji reactions via Signal RPC
  • on_processing_start(event) — adds 👀 when processing begins
  • on_processing_complete(event, success) — swaps to ✅ or ❌ on completion
  • message_id propagated from inbound message timestamp on MessageEvent
  • 913 lines of new test coverage in tests/gateway/test_signal.py

3. Agent budget reset after context compression

  • IterationBudget.reset(new_max=None) — zeroes the counter; optionally updates max_total
  • After each _compress_context() pass: api_call_count = 0; self.iteration_budget.reset() so the agent gets a fresh budget for the continuation session instead of exiting immediately post-compaction
  • DEFAULT_MAX_ITERATIONS: 50 → 90 in delegate_tool.py (complex subtasks need more runway)
  • _HEARTBEAT_INTERVAL: 30s → 10s (faster parent touch prevents gateway timeout during long subtasks)
  • contrib/plugins/ccc_integration/: optional plugin for CCC queue heartbeating (install separately)

4. Ctrl+D: readline/bash/zsh semantics

Upstream handle_ctrl_d unconditionally exits. This restores standard behavior: forward-delete the character under the cursor when the buffer is non-empty; only exit when the buffer is empty. Fixes issue #6448.


Test plan

  • Single-workspace Slack (env var path): existing tests pass, no behavior change
  • Multi-workspace Slack: send to both workspaces, receive events from both
  • Revoke one token mid-run: gateway continues with valid accounts
  • Restart gateway: channels previously seen still route to correct workspace
  • Signal edit_message: edit an existing message via Signal RPC
  • Signal reactions: 👀 on processing start, ✅/❌ on complete
  • Agent with large context: compression resets budget, agent continues rather than exiting
  • Ctrl+D mid-word: deletes character; Ctrl+D on empty input: exits session
  • pytest tests/gateway/test_slack.py tests/gateway/test_signal.py -q — all pass

Relation to existing PRs

🤖 Generated with Claude Code

jordanhubbard and others added 7 commits April 21, 2026 18:50
Each Slack workspace now gets its own AsyncApp + AsyncSocketModeHandler,
enabling the agent to receive events from multiple workspaces simultaneously
via independent Socket Mode connections.

Architecture (modeled after OpenClaw's proven pattern):
- New _load_accounts() reads ~/.hermes/slack_accounts.json for multi-workspace
  configs, each with its own bot_token + app_token pair
- Falls back to SLACK_BOT_TOKEN + SLACK_APP_TOKEN env vars (single workspace)
- _register_app_handlers() extracted to register Bolt event/command/action
  handlers identically on each per-account AsyncApp instance
- connect() iterates accounts, creating independent App+Handler per account
- disconnect() closes all handlers and releases all token locks
- self._app retained as primary for backward compatibility
- Legacy comma-separated bot tokens still work (send-only multi-workspace)

State changes:
- self._handler → self._handlers (Dict[name, handler])
- self._socket_mode_task → self._socket_mode_tasks (Dict[name, task])
- self._token_lock_identity → self._token_lock_identities (list)
- New: self._apps (Dict[name, AsyncApp])

All 82 existing tests pass (64 slack + 18 approval buttons).

(cherry picked from commit e735e15)
…ket Mode

- Graceful token degradation: auth_test failures skip the account instead
  of aborting connect(); only fails if zero accounts authenticate
- Persisted channel→team routing via slack_channel_teams.json: routing
  survives gateway restarts, eliminating post-restart channel_not_found
- Metadata-aware client selection: _get_client(chat_id, metadata) uses
  explicit team_id from metadata before falling back to learned routing;
  all send/upload methods pass metadata through
- _record_channel_team() replaces direct _channel_team[] assignments and
  persists on every new mapping
- Three new tests: stale token skipping, persisted routing, inbound recording

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit f205851)
…ange

Upstream added mrkdwn=True to all chat_postMessage calls; update assertion
to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 1f9815d)
Restores standard readline/bash/zsh behaviour: ^D forward-deletes the
character under the cursor when there is text in the buffer, and only
exits the session when the buffer is empty. Matches upstream fix in
#4783 (issue #6448).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 459dd50)
(cherry picked from commit 2404e65)
…y + CCC integration

run_agent.py:
- IterationBudget.reset(): new method to zero the counter (and optionally
  update max_total) — called after context compression so the agent gets
  a fresh budget for the continuation session instead of immediately exiting
- Reset api_call_count=0 and iteration_budget after _compress_context() so
  the while-loop condition doesn't immediately fail post-compaction
- Fix 'completed' formula: was 'final_response is not None and api_calls <
  max_iterations' which incorrectly marked budget-summary runs as incomplete.
  Now uses 'final_response is not None and last_message_role != tool' —
  a summary from _handle_max_iterations counts as completed

tools/delegate_tool.py:
- DEFAULT_MAX_ITERATIONS: 50 → 90 (complex tasks need more runway)
- _HEARTBEAT_INTERVAL: 30s → 10s (faster parent touch prevents gateway
  timeout warning from firing during long subtasks)

contrib/plugins/ccc_integration/:
- New hermes plugin: posts CCC heartbeats on session start and every 3
  LLM calls; marks queue items complete/fail on session end via
  /api/item/:id/complete|fail (reads CCC_QUEUE_ITEM_ID from env)
- Install: cp -r contrib/plugins/ccc_integration ~/.hermes/plugins/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit f4b0f99)
- Propagate inbound message timestamp as message_id on MessageEvent
- Add edit_message() using Signal's editTimestamp RPC parameter
- Add _send_reaction() with on_processing_start (👀) / on_processing_complete (✅/❌) hooks
- Add per-workspace user_token (xoxp-) support to Slack adapter
- Add 913 lines of new Signal adapter test coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit ce8495f)
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists platform/slack Slack app adapter platform/signal Signal CLI adapter comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/delegate Subagent delegation labels Apr 22, 2026
@trevorgordon981

Copy link
Copy Markdown
Contributor

I have verified this solution by inspecting the code changes and the extensive new test suite (909 lines). The fix adds full support for Signal reactions (emoji responses), message editing via , and precise tracking. It correctly parses incoming reaction events, maps them to Hermes's internal format, and enables edits on previously sent messages. The Slack adapter also receives minor multi-workspace Socket Mode improvements. This brings Signal to feature parity with Slack and Telegram.

Tested and confirmed. ✅

@kshitijk4poor

Copy link
Copy Markdown
Contributor

Thanks for the breadth of work here. Closing as stale/incoherent to merge as a unit: the branch is ~6947 commits behind and the diff removes ~1.2M lines (reverting the current site/docs/dashboard tree), and it bundles ~7 unrelated changes (Slack Socket Mode, Ctrl+D fix, budget reset, Signal reactions/editing, etc.). Signal reactions already shipped on main. If you'd still like the Signal edit_message() slice (signal-cli editTimestamp) or Slack multi-workspace, those would each be welcome as a focused PR against current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/signal Signal CLI adapter platform/slack Slack app adapter tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants