Skip to content

slack-channel-monitor: poll thread messages and reuse thread conversations#289

Open
rbren wants to merge 2 commits into
mainfrom
slack-monitor-thread-messages
Open

slack-channel-monitor: poll thread messages and reuse thread conversations#289
rbren wants to merge 2 commits into
mainfrom
slack-monitor-thread-messages

Conversation

@rbren

@rbren rbren commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

The Slack channel monitor previously only saw top-level channel messages. Triggers posted as replies inside threads were invisible unless the thread already had an active OpenHands conversation. This PR extends the poller to look at messages inside threads too, and ensures that when a thread already has an open conversation we reuse it instead of spawning a duplicate.

Behavior changes (scripts/main.py)

  • Thread discovery. Every top-level message returned by conversations.history with reply_count > 0 is registered as a known thread; replies for each known thread are fetched on every run via conversations.replies.
  • Persistence across runs. A new state["known_threads"] map ({channel_id: {thread_ts: latest_reply_ts}}) keeps threads pollable even after the parent message ages out of the channel history window. Capped at MAX_KNOWN_THREADS_PER_CHANNEL = 50 (oldest by latest_reply_ts pruned first) so the worst-case conversations.replies call count stays well under Slack Tier 3 limits. Entries for unmonitored channels are dropped.
  • Per-thread conversation reuse. The main loop is unified: if a thread already has an open OpenHands conversation, every in-thread message - plain replies and in-thread trigger phrases alike - is forwarded to that conversation. New conversations are only created when there is no open conversation for the thread.
  • Thread-scoped context. When the trigger arrives inside a thread, the prompt now includes the thread's own messages (parent + earlier replies) instead of channel-wide history. Top-level triggers still use channel context.

Docs updated

  • SKILL.md Runtime Behaviour: thread polling, known_threads, conversation-reuse rule.
  • README.md Features / How-It-Works: thread-aware polling + per-thread conversation reuse.
  • references/state-schema.md: documents known_threads with schema, pruning rules, and example.
  • references/slack-api.md: updated conversations.replies rate-limit row.

Verification

  • python3 -m py_compile skills/slack-channel-monitor/scripts/main.py passes.
  • python3 scripts/sync_extensions.py --check is clean (only the pre-existing unrelated coverage warning for plugins/issue-duplicate-checker).
  • Inline smoke tests confirm:
    • _prune_known_threads enforces the per-channel cap and drops unmonitored channels.
    • _poll_new_messages discovers new thread parents from history, polls both newly-discovered and pre-existing known threads, updates known_threads with the most recent reply ts, and returns top-level + reply messages sorted chronologically.

This PR was opened by an AI agent (OpenHands) on behalf of @rbren.

rbren and others added 2 commits June 2, 2026 11:10
The poller previously fetched conversations.replies only for threads that
already had an active OpenHands conversation, so a trigger phrase posted
as a reply inside an untracked thread was never seen.

Changes:
- Discover thread parents from each channel's history (any message with
  reply_count > 0) and persist them in a new state.known_threads map so
  their replies remain pollable after the parent ages out of the history
  window.
- Poll conversations.replies for every tracked-conversation thread plus
  every entry in known_threads (capped at 50 per channel to stay under
  Slack Tier 3 limits).
- Unify the main message loop: if a thread already has an open
  conversation, forward every in-thread message to it (trigger phrases
  and plain replies alike) instead of creating a duplicate conversation.
- Use thread-scoped context (parent + earlier replies) when a trigger
  arrives inside a thread; keep channel-wide context for top-level
  triggers.
- Update SKILL.md, README.md, slack-api.md, and state-schema.md to
  reflect the new behaviour and state field.

Co-authored-by: openhands <openhands@all-hands.dev>
The previous template wrote the context block as:

    Recent channel context (oldest -> newest):
    ---
    [U1]: ...
    [U2]: ...
    ---

CommonMark interprets a line of text followed by '---' on the next line
as a setext H2 heading, so both the context label *and* the context
block itself were being rendered as huge headings in the conversation
UI.

Switch to a 4-backtick fenced code block instead. This:

* eliminates the setext-heading collision entirely,
* makes the rendered output a clearly demarcated monospace block
  signalling 'this is data, not prose', and
* prevents markdown-looking characters in user-supplied Slack text
  (# foo, * bar, _baz_, triple-backticks) from rendering as markdown
  when the conversation is displayed.

Verified with markdown_it (CommonMark): the rendered HTML now contains
zero headings; the context label is a paragraph; the context block is
a <pre><code> with all special characters preserved verbatim.

Co-authored-by: openhands <openhands@all-hands.dev>
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.

1 participant