Session Binding Channel Agnostic Plan
Session Binding Channel Agnostic Plan
Overview
This document defines the long term channel agnostic session binding model and the concrete scope for the next implementation iteration.
Goal:
- make subagent bound session routing a core capability
- keep channel specific behavior in adapters
- avoid regressions in normal Discord behavior
Why this exists
Current behavior mixes:
- completion content policy
- destination routing policy
- Discord specific details
This caused edge cases such as:
- duplicate main and thread delivery under concurrent runs
- stale token usage on reused binding managers
- missing activity accounting for webhook sends
Iteration 1 scope
This iteration is intentionally limited.
1. Add channel agnostic core interfaces
Add core types and service interfaces for bindings and routing.
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
Add a single destination resolution path for completion events.
Router contract:
export interface BoundDeliveryRouter { resolveDestination(input: { eventKind: "task_completion"; targetSessionKey: string; requester?: ConversationRef; failClosed: boolean; }): { binding: SessionBindingRecord | null; mode: "bound" | "fallback"; reason: string; };}For this iteration:
- only
task_completionis routed through this new path - existing paths for other event kinds remain as-is
3. Keep Discord as adapter
Discord remains the first adapter implementation.
Adapter responsibilities:
- create/reuse thread conversations
- send bound messages via webhook or channel send
- validate thread state (archived/deleted)
- map adapter metadata (webhook identity, thread ids)
4. Fix currently known correctness issues
Required in this iteration:
- refresh token usage when reusing existing thread binding manager
- record outbound activity for webhook based Discord sends
- stop implicit main channel fallback when a bound thread destination is selected for session mode completion
5. Preserve current runtime safety defaults
No behavior change for users with thread bound spawn disabled.
Defaults stay:
channels.discord.threadBindings.spawnSubagentSessions = false
Result:
- normal Discord users stay on current behavior
- new core path affects only bound session completion routing where enabled
Not in iteration 1
Explicitly deferred:
- ACP binding targets (
targetKind: "acp") - new channel adapters beyond Discord
- global replacement of all delivery paths (
spawn_ack, futuresubagent_message) - protocol level changes
- store migration/versioning redesign for all binding persistence
Notes on ACP:
- interface design keeps room for ACP
- ACP implementation is not started in this iteration
Routing invariants
These invariants are mandatory for iteration 1.
- destination selection and content generation are separate steps
- if session mode completion resolves to an active bound destination, delivery must target that destination
- no hidden reroute from bound destination to main channel
- fallback behavior must be explicit and observable
Compatibility and rollout
Compatibility target:
- no regression for users with thread bound spawning off
- no change to non-Discord channels in this iteration
Rollout:
- Land interfaces and router behind current feature gates.
- Route Discord completion mode bound deliveries through router.
- Keep legacy path for non-bound flows.
- Verify with targeted tests and canary runtime logs.
Tests required in iteration 1
Unit and integration coverage required:
- manager token rotation uses latest token after manager reuse
- webhook sends update channel activity timestamps
- two active bound sessions in same requester channel do not duplicate to main channel
- completion for bound session mode run resolves to thread destination only
- disabled spawn flag keeps legacy behavior unchanged
Proposed implementation files
Core:
src/infra/outbound/session-binding-service.ts(new)src/infra/outbound/bound-delivery-router.ts(new)src/agents/subagent-announce.ts(completion destination resolution integration)
Discord adapter and runtime:
src/discord/monitor/thread-bindings.manager.tssrc/discord/monitor/reply-delivery.tssrc/discord/send.outbound.ts
Tests:
src/discord/monitor/provider*.test.tssrc/discord/monitor/reply-delivery.test.tssrc/agents/subagent-announce.format.test.ts
Done criteria for iteration 1
- core interfaces exist and are wired for completion routing
- correctness fixes above are merged with tests
- no main and thread duplicate completion delivery in session mode bound runs
- no behavior change for disabled bound spawn deployments
- ACP remains explicitly deferred