Skip to content

Session Binding Channel Agnostic Plan

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

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

This iteration is intentionally limited.

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

Section titled “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_completion is routed through this new path
  • existing paths for other event kinds remain as-is

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)

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

Section titled “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

Explicitly deferred:

  • ACP binding targets (targetKind: "acp")
  • new channel adapters beyond Discord
  • global replacement of all delivery paths (spawn_ack, future subagent_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

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 target:

  • no regression for users with thread bound spawning off
  • no change to non-Discord channels in this iteration

Rollout:

  1. Land interfaces and router behind current feature gates.
  2. Route Discord completion mode bound deliveries through router.
  3. Keep legacy path for non-bound flows.
  4. Verify with targeted tests and canary runtime logs.

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

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.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/send.outbound.ts

Tests:

  • src/discord/monitor/provider*.test.ts
  • src/discord/monitor/reply-delivery.test.ts
  • src/agents/subagent-announce.format.test.ts
  • 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