Skip to content

feat: add slack-channel-monitor skill#258

Merged
tofarr merged 8 commits into
mainfrom
feat/slack-channel-monitor
May 22, 2026
Merged

feat: add slack-channel-monitor skill#258
tofarr merged 8 commits into
mainfrom
feat/slack-channel-monitor

Conversation

@tofarr
Copy link
Copy Markdown
Contributor

@tofarr tofarr commented May 22, 2026

Summary

Adds a new skill that guides users through setting up a cron automation to monitor up to 10 Slack channels and start OpenHands conversations when a configurable trigger phrase is detected.

Skill: slack-channel-monitor

What it does

When invoked, the skill walks the user through:

  1. Verifying a Slack bot or user token is available as a secret
  2. Resolving channel names to IDs (with graceful handling of permission errors)
  3. Collecting a trigger phrase (default: @openhands)
  4. Generating and uploading a customised automation script
  5. Creating the automation with cron schedule * * * * *

Each polling run:

  • Adds a 👀 reaction to any message containing the trigger phrase
  • Creates an OpenHands conversation pre-loaded with the message and recent channel context
  • Posts a reply in the Slack thread with a link to the conversation
  • For- For- For- For- For- For- Fore running conversation
  • Detects terminal/idle conversation states and posts the agent's final response back to Slack

Files

File Purpose
SKILL.md Setup workflow and runtime behaviour reference
scripts/main.py Pure stdlib automation script (no SDK required)
references/slack-api.md Token types, required scopes, endpoint reference, rate limits
`references/stat `references/stat

Screenshots

Create an automation with a prompt:
image

image

This creates an automation:
image

Which monitors the channel...
image

Creates conversations...
image

And posts messages back to slack...
image

Technical notes

  • Polling strategy: user token + search:read + >1 channel uses a single search.messages call; otherwise one conversations.history call per channel + conversations.replies per active thread
  • **State pers- **State pers- d at {WORKSPACE_BASE_ROOT}/automation-state/slack_poller_{automation_id}.json across runs
  • Conversation link: posted immediately on trigger detection; configurable via OPENHANDS_URL secret
  • Done detection: API status in {idle, finished, error, stuck} with a 15-second debounce to avoid same-run false positives
  • Local mode only: targets the dev:automation local stack; a cloud/webhook variant is a separate concern

This PR was created by an AI agent (OpenHands) on behalf of @tofarr.

Adds a skill that guides users through creating a cron automation to
monitor up to 10 Slack channels and start OpenHands conversations when
a configurable trigger phrase is detected.

Key features:
- Polls channels every minute via cron (* * * * *)
- Trigger phrase detection with configurable default (@OpenHands)
- Resolves channel names to IDs; handles permission errors gracefully
- Single search.messages call for multi-channel user tokens with
  search:read; falls back to per-channel conversations.history
- Thread replies forwarded to running conversations
- 👀 reaction on trigger messages
- Conversation link posted immediately in the Slack thread
- Terminal/idle state detection with debounce; posts agent final
  response as summary; error/stuck states post a clear error notice
- State persisted across runs in automation-state/slack_poller_*.json

Includes:
- scripts/main.py  - pure stdlib automation script (no SDK needed)
- references/slack-api.md    - token types, scopes, endpoints, limits
- references/state-schema.md - JSON state schema and lifecycle diagram

Co-authored-by: openhands <openhands@all-hands.dev>
Required by tests/test_skills_have_readme.py.

Co-authored-by: openhands <openhands@all-hands.dev>
- Replace get_bot_user_id with _slack_auth_test which reads the
  X-OAuth-Scopes response header from auth.test. Fail fast with a clear
  RuntimeError if the token lacks channels:history (or equivalent) or
  chat:write - no point running the poll loop without read/write access.
- Set can_react from the resolved scopes and guard both add_reaction
  call sites behind it, so a missing reactions:write scope causes a
  one-time note rather than a warning on every trigger message.
- Add workspace field to create_conversation (POST /api/conversations
  requires it); use WORKSPACE_BASE env var, defaulting to /workspace.

Co-authored-by: openhands <openhands@all-hands.dev>
Required by tests/test_skill_plugin_loading.py:
- .plugin/plugin.json  (Codex and Claude Code manifest)
- .claude-plugin -> .plugin  (symlink)
- .codex-plugin  -> .plugin  (symlink)

Co-authored-by: openhands <openhands@all-hands.dev>
POST /api/conversations requires either 'agent' or 'agent_settings'.
Add _get_agent_settings() which calls GET /api/settings and extracts the
configured agent_settings block (LLM model, agent kind, etc.), then
pass it through in create_conversation so the new conversation inherits
the server's LLM configuration rather than failing validation.

Co-authored-by: openhands <openhands@all-hands.dev>
The agent_settings code path has a double-registration bug: Pydantic
calls create_agent() during StartConversationRequest validation to
populate the agent field, then StoredConversation construction runs the
same initialisation again - both attempts try to register the LLM with
usage_id='default' in the per-conversation registry, and the second
call raises ValueError('Usage ID already exists in registry').

Fix by switching to the same approach the SDK uses:
- Fetch GET /api/settings with X-Expose-Secrets: plaintext to get the
  real (unmasked) LLM api_key
- Build an 'agent' dict directly {kind: Agent, llm: <settings.llm>}
- Pass it as the 'agent' field (not 'agent_settings') so Pydantic
  validates the dict via AgentBase.model_validate() without triggering
  create_agent(), and StoredConversation construction only registers
  the LLM once

Also drops secrets_encrypted: True (no longer needed) and removes the
now-unused uuid import.

Co-authored-by: openhands <openhands@all-hands.dev>
When initial_message is provided, conversation_service.py calls
send_message(message, run=True) which starts the agent immediately.
Our subsequent POST to /api/conversations/{id}/run therefore always
returned 409 'Conversation already running', which propagated as an
exception out of create_conversation before conv_id was returned.

The knock-on effects were:
- active_convs was never updated, so the conversation was never tracked
- The 'On it!' Slack reply and all follow-up (completion detection,
  posting the verdict) were silently skipped

Fix: drop the /run call entirely - the server handles it.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr tofarr marked this pull request as ready for review May 22, 2026 16:57
@tofarr tofarr requested a review from all-hands-bot May 22, 2026 16:58
Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taste Rating: 🟡 Acceptable

This is a well-structured skill that solves a real problem (Slack-OpenHands integration). The implementation is functional and the documentation is comprehensive. However, there are important architectural issues that should be addressed:

  1. Function complexity: The main() function is 284 lines and violates the max-3-indentation rule in multiple places
  2. Testing gap: Complex stateful logic lacks automated tests for core functions
  3. Code organization: Several opportunities to extract reusable helpers

The code works (evidenced by screenshots) but needs refactoring for long-term maintainability.

[TESTING GAPS]

This PR adds 664 lines of complex automation logic with stateful behavior (message tracking, conversation lifecycle management), but provides no automated tests. The repository has test infrastructure and similar components (pr-review, qa-changes, release-notes) include tests.

Core logic functions that should be unit tested:

  • _is_human_message() - message filtering logic with multiple conditions
  • load_state() / save_state() - state persistence with schema versioning
  • Thread root timestamp extraction (lines 515-516)
  • Bot message timestamp tracking/trimming (lines 643-647)
  • Conversation key generation ("{channel_id}:{thread_ts}")

While full integration tests would require mocking Slack and OpenHands APIs, these pure logic functions can be tested without external dependencies. Add unit tests for the core message processing and state management logic.

[RISK ASSESSMENT]

⚠️ Risk Assessment: 🟡 MEDIUM

Risk factors:

  • Complexity: 284-line main() function with multiple nested control flows increases bug surface area
  • State management: Persistent JSON state across runs with no validation - corrupted state could cause silent failures
  • External dependencies: Integrates with two external APIs (Slack + OpenHands) with error handling that sometimes silently continues
  • Testing: No automated tests to catch regressions in message filtering or state transitions

Mitigating factors:

  • Manual testing confirms basic functionality (screenshots show end-to-end flow)
  • Stdlib-only implementation reduces dependency risk
  • Fire-and-forget automation pattern (failures don't block critical paths)
  • Clear documentation and reference materials

VERDICT:
Worth merging - Functional implementation solves a real need, but follow-up refactoring recommended for long-term maintainability.

KEY INSIGHT:
The complexity is concentrated in a single 284-line function. Extract 4-5 focused helpers to bring indentation back under control and make the logic testable.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/extensions/actions/runs/26301032233

Comment thread skills/slack-channel-monitor/scripts/main.py Outdated
Comment thread skills/slack-channel-monitor/scripts/main.py Outdated
Comment thread skills/slack-channel-monitor/scripts/main.py Outdated
Comment thread skills/slack-channel-monitor/scripts/main.py Outdated
Comment thread skills/slack-channel-monitor/scripts/main.py
- Improve constant comments (INITIAL_LOOKBACK, DONE_DEBOUNCE, MAX_BOT_TS,
  CONTEXT_MESSAGE_LIMIT) with fuller explanations of intent
- Add CONTEXT_LOOKBACK_SECONDS constant to name the magic 3600 value
- Change has_search_permission to accept scopes set[str] instead of
  making a redundant API call (scopes already available from auth.test)
- Extract _gather_channel_context() helper to eliminate 5-level nesting
- Break main() (284 lines) into focused helper functions:
    _resolve_slack_token(), _verify_token_scopes(), _poll_new_messages(),
    _process_trigger_message(), _check_conversation_completion()

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Copy Markdown

@hieptl hieptl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! 🙏

@tofarr tofarr merged commit 19a5613 into main May 22, 2026
4 checks passed
@tofarr tofarr deleted the feat/slack-channel-monitor branch May 22, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants