first commit
This commit is contained in:
324
content/experiments/plans/session-binding-channel-agnostic.md
Normal file
324
content/experiments/plans/session-binding-channel-agnostic.md
Normal file
@@ -0,0 +1,324 @@
|
||||
---
|
||||
summary: "Channel agnostic session binding architecture and iteration 1 delivery scope"
|
||||
read_when:
|
||||
- Refactoring channel-agnostic session routing and bindings
|
||||
- Investigating duplicate, stale, or missing session delivery across channels
|
||||
owner: "onutc"
|
||||
status: "in-progress"
|
||||
last_updated: "2026-02-21"
|
||||
title: "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:
|
||||
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
|
||||
### 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`, 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
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## 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.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`
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user