Skip to content

Commit 8178ea4

Browse files
Onurthewilloftheshadow
andauthored
feat: thread-bound subagents on Discord (#21805)
* docs: thread-bound subagents plan * docs: add exact thread-bound subagent implementation touchpoints * Docs: prioritize auto thread-bound subagent flow * Docs: add ACP harness thread-binding extensions * Discord: add thread-bound session routing and auto-bind spawn flow * Subagents: add focus commands and ACP/session binding lifecycle hooks * Tests: cover thread bindings, focus commands, and ACP unbind hooks * Docs: add plugin-hook appendix for thread-bound subagents * Plugins: add subagent lifecycle hook events * Core: emit subagent lifecycle hooks and decouple Discord bindings * Discord: handle subagent bind lifecycle via plugin hooks * Subagents: unify completion finalizer and split registry modules * Add subagent lifecycle events module * Hooks: fix subagent ended context key * Discord: share thread bindings across ESM and Jiti * Subagents: add persistent sessions_spawn mode for thread-bound sessions * Subagents: clarify thread intro and persistent completion copy * test(subagents): stabilize sessions_spawn lifecycle cleanup assertions * Discord: add thread-bound session TTL with auto-unfocus * Subagents: fail session spawns when thread bind fails * Subagents: cover thread session failure cleanup paths * Session: add thread binding TTL config and /session ttl controls * Tests: align discord reaction expectations * Agent: persist sessionFile for keyed subagent sessions * Discord: normalize imports after conflict resolution * Sessions: centralize sessionFile resolve/persist helper * Discord: harden thread-bound subagent session routing * Rebase: resolve upstream/main conflicts * Subagents: move thread binding into hooks and split bindings modules * Docs: add channel-agnostic subagent routing hook plan * Agents: decouple subagent routing from Discord * Discord: refactor thread-bound subagent flows * Subagents: prevent duplicate end hooks and orphaned failed sessions * Refactor: split subagent command and provider phases * Subagents: honor hook delivery target overrides * Discord: add thread binding kill switches and refresh plan doc * Discord: fix thread bind channel resolution * Routing: centralize account id normalization * Discord: clean up thread bindings on startup failures * Discord: add startup cleanup regression tests * Docs: add long-term thread-bound subagent architecture * Docs: split session binding plan and dedupe thread-bound doc * Subagents: add channel-agnostic session binding routing * Subagents: stabilize announce completion routing tests * Subagents: cover multi-bound completion routing * Subagents: suppress lifecycle hooks on failed thread bind * tests: fix discord provider mock typing regressions * docs/protocol: sync slash command aliases and delete param models * fix: add changelog entry for Discord thread-bound subagents (#21805) (thanks @onutc) --------- Co-authored-by: Shadow <[email protected]>
1 parent 166068d commit 8178ea4

114 files changed

Lines changed: 12196 additions & 1641 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
- Discord: add configurable ephemeral defaults for slash-command responses. (#16563) Thanks @wei.
1818
- Discord: support updating forum `available_tags` via channel edit actions for forum tag management. (#12070) Thanks @xiaoyaner0201.
1919
- Discord: include channel topics in trusted inbound metadata on new sessions. Thanks @thewilloftheshadow.
20+
- Discord/Subagents: add thread-bound subagent sessions on Discord with per-thread focus/list controls and thread-bound continuation routing for spawned helper agents. (#21805) Thanks @onutc.
2021
- iOS/Chat: clean chat UI noise by stripping inbound untrusted metadata/timestamp prefixes, formatting tool outputs into concise summaries/errors, compacting the composer while typing, and supporting tap-to-dismiss keyboard in chat view. (#22122) thanks @mbelinky.
2122
- iOS/Watch: bridge mirrored watch prompt notification actions into iOS quick-reply handling, including queued action handoff until app model initialization. (#22123) thanks @mbelinky.
2223
- iOS/Gateway: stabilize background wake and reconnect behavior with background reconnect suppression/lease windows, BGAppRefresh wake fallback, location wake hook throttling, and APNs wake retry+nudge instrumentation. (#21226) thanks @mbelinky.

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,17 +1191,21 @@ public struct SessionsResetParams: Codable, Sendable {
11911191
public struct SessionsDeleteParams: Codable, Sendable {
11921192
public let key: String
11931193
public let deletetranscript: Bool?
1194+
public let emitlifecyclehooks: Bool?
11941195

11951196
public init(
11961197
key: String,
1197-
deletetranscript: Bool?
1198+
deletetranscript: Bool?,
1199+
emitlifecyclehooks: Bool?
11981200
) {
11991201
self.key = key
12001202
self.deletetranscript = deletetranscript
1203+
self.emitlifecyclehooks = emitlifecyclehooks
12011204
}
12021205
private enum CodingKeys: String, CodingKey {
12031206
case key
12041207
case deletetranscript = "deleteTranscript"
1208+
case emitlifecyclehooks = "emitLifecycleHooks"
12051209
}
12061210
}
12071211

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,17 +1191,21 @@ public struct SessionsResetParams: Codable, Sendable {
11911191
public struct SessionsDeleteParams: Codable, Sendable {
11921192
public let key: String
11931193
public let deletetranscript: Bool?
1194+
public let emitlifecyclehooks: Bool?
11941195

11951196
public init(
11961197
key: String,
1197-
deletetranscript: Bool?
1198+
deletetranscript: Bool?,
1199+
emitlifecyclehooks: Bool?
11981200
) {
11991201
self.key = key
12001202
self.deletetranscript = deletetranscript
1203+
self.emitlifecyclehooks = emitlifecyclehooks
12011204
}
12021205
private enum CodingKeys: String, CodingKey {
12031206
case key
12041207
case deletetranscript = "deleteTranscript"
1208+
case emitlifecyclehooks = "emitLifecycleHooks"
12051209
}
12061210
}
12071211

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
summary: "Channel agnostic session binding architecture and iteration 1 delivery scope"
3+
owner: "onutc"
4+
status: "in-progress"
5+
last_updated: "2026-02-21"
6+
title: "Session Binding Channel Agnostic Plan"
7+
---
8+
9+
# Session Binding Channel Agnostic Plan
10+
11+
## Overview
12+
13+
This document defines the long term channel agnostic session binding model and the concrete scope for the next implementation iteration.
14+
15+
Goal:
16+
17+
- make subagent bound session routing a core capability
18+
- keep channel specific behavior in adapters
19+
- avoid regressions in normal Discord behavior
20+
21+
## Why this exists
22+
23+
Current behavior mixes:
24+
25+
- completion content policy
26+
- destination routing policy
27+
- Discord specific details
28+
29+
This caused edge cases such as:
30+
31+
- duplicate main and thread delivery under concurrent runs
32+
- stale token usage on reused binding managers
33+
- missing activity accounting for webhook sends
34+
35+
## Iteration 1 scope
36+
37+
This iteration is intentionally limited.
38+
39+
### 1. Add channel agnostic core interfaces
40+
41+
Add core types and service interfaces for bindings and routing.
42+
43+
Proposed core types:
44+
45+
```ts
46+
export type BindingTargetKind = "subagent" | "session";
47+
export type BindingStatus = "active" | "ending" | "ended";
48+
49+
export type ConversationRef = {
50+
channel: string;
51+
accountId: string;
52+
conversationId: string;
53+
parentConversationId?: string;
54+
};
55+
56+
export type SessionBindingRecord = {
57+
bindingId: string;
58+
targetSessionKey: string;
59+
targetKind: BindingTargetKind;
60+
conversation: ConversationRef;
61+
status: BindingStatus;
62+
boundAt: number;
63+
expiresAt?: number;
64+
metadata?: Record<string, unknown>;
65+
};
66+
```
67+
68+
Core service contract:
69+
70+
```ts
71+
export interface SessionBindingService {
72+
bind(input: {
73+
targetSessionKey: string;
74+
targetKind: BindingTargetKind;
75+
conversation: ConversationRef;
76+
metadata?: Record<string, unknown>;
77+
ttlMs?: number;
78+
}): Promise<SessionBindingRecord>;
79+
80+
listBySession(targetSessionKey: string): SessionBindingRecord[];
81+
resolveByConversation(ref: ConversationRef): SessionBindingRecord | null;
82+
touch(bindingId: string, at?: number): void;
83+
unbind(input: {
84+
bindingId?: string;
85+
targetSessionKey?: string;
86+
reason: string;
87+
}): Promise<SessionBindingRecord[]>;
88+
}
89+
```
90+
91+
### 2. Add one core delivery router for subagent completions
92+
93+
Add a single destination resolution path for completion events.
94+
95+
Router contract:
96+
97+
```ts
98+
export interface BoundDeliveryRouter {
99+
resolveDestination(input: {
100+
eventKind: "task_completion";
101+
targetSessionKey: string;
102+
requester?: ConversationRef;
103+
failClosed: boolean;
104+
}): {
105+
binding: SessionBindingRecord | null;
106+
mode: "bound" | "fallback";
107+
reason: string;
108+
};
109+
}
110+
```
111+
112+
For this iteration:
113+
114+
- only `task_completion` is routed through this new path
115+
- existing paths for other event kinds remain as-is
116+
117+
### 3. Keep Discord as adapter
118+
119+
Discord remains the first adapter implementation.
120+
121+
Adapter responsibilities:
122+
123+
- create/reuse thread conversations
124+
- send bound messages via webhook or channel send
125+
- validate thread state (archived/deleted)
126+
- map adapter metadata (webhook identity, thread ids)
127+
128+
### 4. Fix currently known correctness issues
129+
130+
Required in this iteration:
131+
132+
- refresh token usage when reusing existing thread binding manager
133+
- record outbound activity for webhook based Discord sends
134+
- stop implicit main channel fallback when a bound thread destination is selected for session mode completion
135+
136+
### 5. Preserve current runtime safety defaults
137+
138+
No behavior change for users with thread bound spawn disabled.
139+
140+
Defaults stay:
141+
142+
- `channels.discord.threadBindings.spawnSubagentSessions = false`
143+
144+
Result:
145+
146+
- normal Discord users stay on current behavior
147+
- new core path affects only bound session completion routing where enabled
148+
149+
## Not in iteration 1
150+
151+
Explicitly deferred:
152+
153+
- ACP binding targets (`targetKind: "acp"`)
154+
- new channel adapters beyond Discord
155+
- global replacement of all delivery paths (`spawn_ack`, future `subagent_message`)
156+
- protocol level changes
157+
- store migration/versioning redesign for all binding persistence
158+
159+
Notes on ACP:
160+
161+
- interface design keeps room for ACP
162+
- ACP implementation is not started in this iteration
163+
164+
## Routing invariants
165+
166+
These invariants are mandatory for iteration 1.
167+
168+
- destination selection and content generation are separate steps
169+
- if session mode completion resolves to an active bound destination, delivery must target that destination
170+
- no hidden reroute from bound destination to main channel
171+
- fallback behavior must be explicit and observable
172+
173+
## Compatibility and rollout
174+
175+
Compatibility target:
176+
177+
- no regression for users with thread bound spawning off
178+
- no change to non-Discord channels in this iteration
179+
180+
Rollout:
181+
182+
1. Land interfaces and router behind current feature gates.
183+
2. Route Discord completion mode bound deliveries through router.
184+
3. Keep legacy path for non-bound flows.
185+
4. Verify with targeted tests and canary runtime logs.
186+
187+
## Tests required in iteration 1
188+
189+
Unit and integration coverage required:
190+
191+
- manager token rotation uses latest token after manager reuse
192+
- webhook sends update channel activity timestamps
193+
- two active bound sessions in same requester channel do not duplicate to main channel
194+
- completion for bound session mode run resolves to thread destination only
195+
- disabled spawn flag keeps legacy behavior unchanged
196+
197+
## Proposed implementation files
198+
199+
Core:
200+
201+
- `src/infra/outbound/session-binding-service.ts` (new)
202+
- `src/infra/outbound/bound-delivery-router.ts` (new)
203+
- `src/agents/subagent-announce.ts` (completion destination resolution integration)
204+
205+
Discord adapter and runtime:
206+
207+
- `src/discord/monitor/thread-bindings.manager.ts`
208+
- `src/discord/monitor/reply-delivery.ts`
209+
- `src/discord/send.outbound.ts`
210+
211+
Tests:
212+
213+
- `src/discord/monitor/provider*.test.ts`
214+
- `src/discord/monitor/reply-delivery.test.ts`
215+
- `src/agents/subagent-announce.format.e2e.test.ts`
216+
217+
## Done criteria for iteration 1
218+
219+
- core interfaces exist and are wired for completion routing
220+
- correctness fixes above are merged with tests
221+
- no main and thread duplicate completion delivery in session mode bound runs
222+
- no behavior change for disabled bound spawn deployments
223+
- ACP remains explicitly deferred

0 commit comments

Comments
 (0)