fix(slack): respond to thread replies without requiring @mention - #1405
Conversation
…mention Two fixes: 1. Host bug: `on_respond` callback never committed workspace writes or injected workspace reader, unlike all other WASM callbacks. Any WASM channel persisting state during on_respond silently lost data. 2. Slack WASM channel: track threads where the bot has participated via workspace storage. When a message event arrives in a channel thread the bot previously replied to, process it without requiring @mention. Closes nearai#1404 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly improves the user experience with the Slack bot by enabling it to maintain context within threads. Users no longer need to repeatedly @mention the bot for follow-up questions or comments in an ongoing thread. This is achieved by introducing thread participation tracking and fixing a host-side issue that prevented the proper persistence of WASM workspace data, ensuring that the bot's memory of active threads is correctly managed and saved. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request introduces a valuable feature allowing the Slack bot to respond to thread replies without requiring an explicit @mention. The changes correctly implement thread tracking in the Slack WASM channel and ensure that workspace writes are committed by the host. However, there are a couple of places where error handling for workspace operations could be improved to prevent silent failures, which are detailed in the review comments.
Address code review feedback: handle the Result from workspace_write when tracking thread participation, logging a warning on failure instead of using `let _ =` which would silently swallow errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
serrrfirat
left a comment
There was a problem hiding this comment.
Medium Severity: Variable Shadowing Confusion
In src/channels/wasm/wrapper.rs around line 1646, the PR changes host_state from immutable to mutable:
Before:
let host_state = Self::extract_host_state(&mut store, &prepared.name, &capabilities);After (this PR):
let mut host_state = Self::extract_host_state(&mut store, &prepared.name, &capabilities);
let pending_writes = host_state.take_pending_writes();
workspace_store.commit_writes(&pending_writes);The change from immutable to mutable is correct (needed for take_pending_writes()), but this suggests the original code had a latent bug where host_state was extracted but never used to commit writes (notice the underscore prefix _host_state at line 1664, indicating it was unused).
Fix: This is actually fixed correctly in the PR. Just noting it confirms the original bug: workspace writes were never committed from on_respond.
serrrfirat
left a comment
There was a problem hiding this comment.
Low Severity: Thread Key Format Validation Missing
In channels-src/slack/src/lib.rs at line 271, the thread key format is:
let thread_key = format!("{}{}/{}", ACTIVE_THREADS_PREFIX, metadata.channel, thread_ts);
// Evaluates to: "state/threads/{channel}/{thread_ts}"There's no validation that channel or thread_ts don't contain forward slashes. While Slack channel IDs are typically alphanumeric (e.g., "C1234567890") and thread_ts is a timestamp (e.g., "1234567890.123456"), so this is likely safe in practice:
Risks:
- No explicit validation - if Slack's format changes or malicious payload is injected, path traversal issues could occur
- Undocumented assumption - code assumes these fields are path-safe
- Potential collisions - channel "C12" with thread "34/56" collides with channel "C12/34" with thread "56"
Fix:
- Add validation:
assert!(!channel.contains('/') && !thread_ts.contains('/')); - Or use URL-safe encoding:
let thread_key = format!("{}{}", ACTIVE_THREADS_PREFIX, urlencoding::encode(&format!("{}/{}", channel, thread_ts))); - Document the key format in the
ACTIVE_THREADS_PREFIXconstant comment
serrrfirat
left a comment
There was a problem hiding this comment.
Low Severity: Missing Cleanup Mechanism
The PR adds thread tracking writes but no cleanup mechanism. Over time, unbounded state accumulates:
- Every thread the bot participates in creates:
state/threads/{channel}/{thread_ts} - No TTL, no expiration, no periodic cleanup
- In a busy workspace: hundreds or thousands of threads per month
- Workspace storage grows indefinitely
Example: 10 channels × 100 threads/month = 1000 new keys/month. After a year: 12,000+ keys.
Fix options:
- TTL-based expiration (simplest): Store timestamp instead of "1", check age on read (see finding #3)
- Periodic cleanup: Background task removes markers older than N days
- Max-threads limit: LRU eviction when count exceeds threshold
- Accept unbounded growth: Document as known limitation in CLAUDE.md
Recommend option 1 (TTL) as it requires minimal changes: store now_millis().to_string() instead of "1", and check now_millis() - parse(value) < 24*3600*1000 on read.
ilblackdragon
left a comment
There was a problem hiding this comment.
Review: fix(slack): respond to thread replies without requiring @mention
Must-fix
-
Thread tracking happens before Slack API success —
workspace_writeat line 269 runs beforechat.postMessage. If the API call fails, the thread is marked as "participated" even though no message was sent. Move tracking to after theslack_response.okcheck (after line 308). -
Fix
call_on_broadcastandcall_on_status— these callbacks have the identical missinginject_workspace_reader+commit_writesbug. Per review discipline: "Fix the pattern, not just the instance."
Should-fix
-
Store timestamp instead of
"1"— enables future TTL-based expiration without migration. Change tochannel_host::time_now().to_string(). -
Add regression test for the host-side workspace commit fix, per project testing rules.
Nice-to-have
- Add TTL-based expiration (e.g., ignore threads older than 7 days) to prevent unbounded growth.
|
Thanks @synner88 for the original fix and the earlier follow-up on the initial review comments. I pushed the remaining hardening changes onto the PR branch, reran the review locally, and the refreshed CI is green on |
|
Addressed the review comments one by one on
CI is green on this revision. |
ilblackdragon
left a comment
There was a problem hiding this comment.
Code Review
The PR is in good shape after addressing all prior review feedback. The core bugs (missing inject_workspace_reader + commit_writes in callbacks, thread tracking before API success) are properly fixed, and the thread participation tracking design is solid — TTL expiry, bounded storage, LRU eviction, and proper error propagation.
Minor suggestions
1. Double prune in track_active_thread (nit)
The second prune_active_threads call is unnecessary — the just-inserted entry has last_seen_millis == now_millis so it can never be TTL-pruned, and we go from at most 256 to 257 entries. A single prune before insert is sufficient:
fn track_active_thread(channel: &str, thread_ts: &str) -> Result<(), String> {
let now_millis = channel_host::now_millis();
let mut active_threads = load_active_threads();
prune_active_threads(&mut active_threads, now_millis);
active_threads.insert(active_thread_key(channel, thread_ts), now_millis);
// second prune_active_threads call removed — at most 257 entries is fine
persist_active_threads(&active_threads)
}2. is_active_thread writes back on read path
Every threaded channel message deserializes the full thread map and potentially writes it back (if pruning removed stale entries). Consider moving the stale-entry cleanup to track_active_thread only (write path) so is_active_thread stays a pure read — avoids unnecessary I/O in busy workspaces.
3. call_on_shutdown not updated
on_shutdown still uses self.capabilities.clone() without inject_workspace_reader. If any WASM channel ever does workspace reads during shutdown, it'll hit the same latent bug this PR fixes. Worth fixing for completeness or documenting that shutdown intentionally doesn't support workspace writes.
Otherwise LGTM — nice work addressing all the earlier feedback.
…rai#1405) * fix(slack): respond to thread replies in channels without requiring @mention Two fixes: 1. Host bug: `on_respond` callback never committed workspace writes or injected workspace reader, unlike all other WASM callbacks. Any WASM channel persisting state during on_respond silently lost data. 2. Slack WASM channel: track threads where the bot has participated via workspace storage. When a message event arrives in a channel thread the bot previously replied to, process it without requiring @mention. Closes nearai#1404 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(slack): log workspace_write error instead of silently discarding Address code review feedback: handle the Result from workspace_write when tracking thread participation, logging a warning on failure instead of using `let _ =` which would silently swallow errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(slack): harden thread reply tracking --------- Co-authored-by: synner88 <29090601+synner88@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Firat Sertgoz <f@nuff.tech> Co-authored-by: firat.sertgoz <firat.sertgoz@near.ai>
Summary
on_respondcallback never commits WASM workspace writes or injects workspace readerCloses #1404
Changes
Host:
src/channels/wasm/wrapper.rsinject_workspace_reader()toon_respondcapabilities (was missing, unlike all other callbacks)workspace_storeinto the async closuretake_pending_writes()+commit_writes()afteron_respondWASM execution completesSlack WASM channel:
channels-src/slack/src/lib.rsACTIVE_THREADS_PREFIXconstant for workspace-persisted thread trackingon_respond(): record thread key (channel/thread_ts) when the bot replies in a threadhandle_slack_event()"message"handler: check if an incoming channel message is a reply in a tracked thread, and process it alongside DMsTest plan
cargo clippy --all --benches --tests --examples --all-featurespasses with zero warnings🤖 Generated with Claude Code