← Back to PRs

#15244: feat: Thread-session binding — route sub-agent I/O through platform threads

by spk-alex open 2026-02-13 06:29 View on GitHub →
channel: msteams channel: slack app: web-ui gateway agents stale size: XL
## Summary Implements thread-session binding for OpenClaw, allowing spawned sub-agents to have their I/O routed through platform threads (Slack, Discord, etc.). Closes #15217 ## What This Adds ### Phase 1: Core Registry - `ThreadBinding` type and `ThreadBindingRegistry` class with bidirectional mapping (thread → sessions, session → thread) - `buildThreadKey()` / `parseThreadKey()` for normalized thread identity - Session store schema extended with optional `threadBinding` field - Registry auto-rebuilds on session store load, syncs on save ### Phase 2: Spawn Integration + Inbound Routing - `sessions_spawn` tool gains `threadBinding` parameter with two modes: - `mode: "bind"` — bind to existing thread (requires `threadId`) - `mode: "create"` — post initial message to channel, use response as thread root - `resolveSessionKeyWithBinding()` — checks registry before falling back to existing suffix-based routing - Slack inbound handler updated to use registry-aware routing - Lifecycle helpers: `bindSessionToThread()`, `unbindSessionFromThread()`, `findSessionsByThread()`, `getSessionThreadBinding()` ### Phase 3: Output Delivery + Cross-Agent Communication - Reply dispatcher checks `threadBinding` for delivery routing: - `thread-only` — deliver to bound thread only - `thread+announcer` — deliver to both thread and requester - `announcer-only` — deliver to requester only - `sessions_send` gains `threadKey` parameter for thread-scoped routing (fanout to all bound sessions) - `sessions_broadcast` tool for broadcasting to all agents in a thread - Graceful fallback when thread delivery fails ## Design Decisions - **Dual routing**: Registry lookup (O(1)) first, falls back to existing suffix-based computed keys — fully backward compatible - **Multi-session bindings**: Multiple agents can bind to one thread (parallel fanout) - **UUID session keys**: Bound sessions use standard UUID keys, binding stored in session data (not key suffix) - **Platform-agnostic**: Registry is channel-plugin agnostic; platform-specific thread creation in Phase 5 ## Files Changed | File | Change | |------|--------| | `src/config/thread-registry.ts` | NEW — Registry, types, lifecycle helpers | | `src/config/sessions/types.ts` | `threadBinding` field on SessionEntry | | `src/config/sessions/store.ts` | Registry rebuild on load, sync on save | | `src/routing/session-key.ts` | `resolveSessionKeyWithBinding()` | | `src/slack/monitor/message-handler/prepare.ts` | Registry-aware inbound routing | | `src/agents/tools/sessions-spawn-tool.ts` | `threadBinding` parameter | | `src/agents/tools/sessions-send-tool.ts` | `threadKey` parameter | | `src/agents/tools/sessions-broadcast-tool.ts` | NEW — Thread broadcast tool | | `src/auto-reply/reply/dispatch-from-config.ts` | Binding-aware delivery routing | ## Tests - 45 new tests across registry, session key resolution, and lifecycle helpers - All existing tests pass (373 total across 49 files) - No type errors ## What's Next (not in this PR) - **Phase 4**: Platform abstraction (`ThreadOperations` interface per channel plugin) - **Phase 5**: Config schema additions, feature flag, documentation <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR adds a persistent thread↔session binding registry (`src/config/thread-registry.ts`) and wires it into session-store load/save so thread bindings can be looked up in O(1) at runtime. Routing is updated to prefer explicit bindings (`resolveSessionKeyWithBinding()`), with Slack inbound handling now consulting the registry before falling back to legacy suffix-based thread keys. It also extends session tools: `sessions_spawn` can bind or create a thread binding on spawn, `sessions_send` can fan out by `threadKey`, and a new `sessions_broadcast` tool broadcasts to all sessions bound to a thread. Main issue to address before merge: reply delivery via `dispatchReplyFromConfig` gates thread routing on `threadBinding.to`, but the binding persisted by the registry/spawn path currently doesn’t set `to`, so thread-bound sessions will never actually route replies to the thread (they’ll continue using announcer/dispatcher behavior). Additionally, both thread fanout tools currently lose which session failed when a send rejects, making partial/error results non-actionable. <h3>Confidence Score: 3/5</h3> - This PR is close to mergeable but has a functional gap in thread-bound reply delivery that should be fixed first. - Core registry + inbound routing changes are straightforward, but the new reply dispatcher thread-routing path is effectively disabled because persisted bindings don’t include the destination (`to`). That means the flagship feature (routing outputs to platform threads) won’t work as intended. Also, fanout tooling reports failures without identifying the failing session, which will complicate operations/debugging. - src/auto-reply/reply/dispatch-from-config.ts, src/agents/tools/sessions-send-tool.ts, src/agents/tools/sessions-broadcast-tool.ts <sub>Last reviewed commit: 37d3b29</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs