Skip to content

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.

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

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

This iteration is intentionally limited so we can get the foundation right.

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
```ts
Export interface BoundDeliveryRouter {
resolveDestination(input: {
eventKind: "task_completion";
targetSessionKey: string;
requester?: ConversationRef;
failClosed: boolean;
}): {
binding: SessionBindingRecord | null;
mode: "bound" | "fallback";
reason: string;
};
}
OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.