Implement OpenClaw Session Binding for Channel Agnostic Rout
Ever felt like your session routing is a tangled mess of platform-specific hacks? You’re trying to send a message, but the logic is so tied to one specific channel that moving it anywhere else feels impossible. We’ve been working on a plan to fix this by moving toward a channel-agnostic session binding model.
Overview
Section titled “Overview”This plan defines how we handle session binding for the long haul. We want to make sure the system is flexible enough to handle any channel while keeping the core logic clean.
Goals:
- make subagent bound session routing a core capability and keep channel specific behavior in adapters
- avoid regressions in normal Discord behavior
Why this exists
Section titled “Why this exists”Right now, our logic is a bit of a mix. We’ve got content policy, routing policy, and Discord-specific details all mashed together in one place.
Current behavior mixes these points:
- completion content and destination routing policies
- Discord specific details
This caused edge cases like:
- duplicate main and thread delivery under concurrent runs and stale token usage on reused binding managers
- missing activity accounting for webhook sends
Iteration 1 scope
Section titled “Iteration 1 scope”This iteration is intentionally limited so we can get the foundation right.
1. Add channel agnostic core interfaces
Section titled “1. Add channel agnostic core interfaces”We’re adding core types and service interfaces to handle bindings and routing properly.
Proposed core types:
export type BindingTargetKind = "subagent" | "session";export type BindingStatus = "active" | "ending" | "ended";
export type ConversationRef = { channel: string; accountId: string; conversationId: string; parentConversationId?: string;};
export type SessionBindingRecord = { bindingId: string; targetSessionKey: string; targetKind: BindingTargetKind; conversation: ConversationRef; status: BindingStatus; boundAt: number; expiresAt?: number; metadata?: Record<string, unknown>;};Core service contract:
export interface SessionBindingService { bind(input: { targetSessionKey: string; targetKind: BindingTargetKind; conversation: ConversationRef; metadata?: Record<string, unknown>; ttlMs?: number; }): Promise<SessionBindingRecord>;
listBySession(targetSessionKey: string): SessionBindingRecord[]; resolveByConversation(ref: ConversationRef): SessionBindingRecord | null; touch(bindingId: string, at?: number): void; unbind(input: { bindingId?: string; targetSessionKey?: string; reason: string; }): Promise<SessionBindingRecord[]>;}2. Add one core delivery router for subagent completions
Section titled “2. Add one core delivery router for subagent completions”We’re introducing a single path to resolve where completion events should go.
Router contract:
export interface BoundDeliveryRouter { resolveDestination(input: { eventKind: "task_completion"; targetSessionKey
```tsExport interface BoundDeliveryRouter { resolveDestination(input: { eventKind: "task_completion"; targetSessionKey: string; requester?: ConversationRef; failClosed: boolean; }): { binding: SessionBindingRecord | null; mode: "bound" | "fallback"; reason: string; };}OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.