Files
openclaw/docs/experiments/plans/thread-bound-subagents.md
Onur 8178ea472d 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 <hi@shadowing.dev>
2026-02-21 16:14:55 +01:00

339 lines
12 KiB
Markdown

---
summary: "Discord thread bound subagent sessions with plugin lifecycle hooks, routing, and config kill switches"
owner: "onutc"
status: "implemented"
last_updated: "2026-02-21"
title: "Thread Bound Subagents"
---
# Thread Bound Subagents
## Overview
This feature lets users interact with spawned subagents directly inside Discord threads.
Instead of only waiting for a completion summary in the parent session, users can move into a dedicated thread that routes messages to the spawned subagent session. Replies are sent in-thread with a thread bound persona.
The implementation is split between channel agnostic core lifecycle hooks and Discord specific extension behavior.
## Goals
- Allow direct thread conversation with a spawned subagent session.
- Keep default subagent orchestration channel agnostic.
- Support both automatic thread creation on spawn and manual focus controls.
- Provide predictable cleanup on completion, kill, timeout, and thread lifecycle changes.
- Keep behavior configurable with global defaults plus channel and account overrides.
## Out of scope
- New ACP protocol features.
- Non Discord thread binding implementations in this document.
- New bot accounts or app level Discord identity changes.
## What shipped
- `sessions_spawn` supports `thread: true` and `mode: "run" | "session"`.
- Spawn flow supports persistent thread bound sessions.
- Discord thread binding manager supports bind, unbind, TTL sweep, and persistence.
- Plugin hook lifecycle for subagents:
- `subagent_spawning`
- `subagent_spawned`
- `subagent_delivery_target`
- `subagent_ended`
- Discord extension implements thread auto bind, delivery target override, and unbind on end.
- Text commands for manual control:
- `/focus`
- `/unfocus`
- `/agents`
- `/session ttl`
- Global and Discord scoped enablement and TTL controls, including a global kill switch.
## Core concepts
### Spawn modes
- `mode: "run"`
- one task lifecycle
- completion announcement flow
- `mode: "session"`
- persistent thread bound session
- supports follow up user messages in thread
Default mode behavior:
- if `thread: true` and mode omitted, mode defaults to `"session"`
- otherwise mode defaults to `"run"`
Constraint:
- `mode: "session"` requires `thread: true`
### Thread binding target model
Bindings are generic targets, not only subagents.
- `targetKind: "subagent" | "acp"`
- `targetSessionKey: string`
This allows the same routing primitive to support ACP/session bindings as well.
### Thread binding manager
The manager is responsible for:
- binding or creating threads for a session target
- unbinding by thread or by target session
- managing webhook reuse and recent unbound webhook echo suppression
- TTL based unbind and stale thread cleanup
- persistence load and save
## Architecture
### Core and extension boundary
Core (`src/agents/*`) does not directly depend on Discord routing internals.
Core emits lifecycle intent through plugin hooks.
Discord extension (`extensions/discord/src/subagent-hooks.ts`) implements Discord specific behavior:
- pre spawn thread bind preparation
- completion delivery target override to bound thread
- unbind on subagent end
### Plugin hook flow
1. `subagent_spawning`
- before run starts
- can block spawn with `status: "error"`
- used to prepare thread binding when `thread: true`
2. `subagent_spawned`
- post run registration event
3. `subagent_delivery_target`
- completion routing override hook
- can redirect completion delivery to bound Discord thread origin
4. `subagent_ended`
- cleanup and unbind signal
### Account ID normalization contract
Thread binding and routing state must use one canonical account id abstraction.
Specification:
- Introduce a shared account id module (proposed: `src/routing/account-id.ts`) and stop defining local normalizers.
- Expose two explicit helpers:
- `normalizeAccountId(value): string`
- returns canonical, defaulted id (current default is `default`)
- use for map keys, manager registration and lookup, persistence keys, routing keys
- `normalizeOptionalAccountId(value): string | undefined`
- returns canonical id when present, `undefined` when absent
- use for inbound optional context fields and merge logic
- Do not implement ad hoc account normalization in feature modules.
- This includes `trim`, `toLowerCase`, or defaulting logic in local helper functions.
- Any map keyed by account id must only accept canonical ids from shared helpers.
- Hook payloads and delivery context should carry raw optional account ids, and normalize at module boundaries only.
Migration guardrails:
- Replace duplicate normalizers in routing, reply payload, command context, and provider helpers with shared helpers.
- Add contract tests that assert identical normalization behavior across:
- route resolution
- thread binding manager lookup
- reply delivery target filtering
- command run context merge
### Persistence and state
Binding state path:
- `${stateDir}/discord/thread-bindings.json`
Record shape contains:
- account, channel, thread
- target kind and target session key
- agent label metadata
- webhook id/token
- boundBy, boundAt, expiresAt
State is stored on `globalThis` to keep one shared registry across ESM and Jiti loader paths.
## Configuration
### Effective precedence
For Discord thread binding options, account override wins, then channel, then global session default, then built in fallback.
- account: `channels.discord.accounts.<id>.threadBindings.<key>`
- channel: `channels.discord.threadBindings.<key>`
- global: `session.threadBindings.<key>`
### Keys
| Key | Scope | Default | Notes |
| ------------------------------------------------------- | --------------- | --------------- | ----------------------------------------- |
| `session.threadBindings.enabled` | global | `true` | master default kill switch |
| `session.threadBindings.ttlHours` | global | `24` | default auto unfocus TTL |
| `channels.discord.threadBindings.enabled` | channel/account | inherits global | Discord override kill switch |
| `channels.discord.threadBindings.ttlHours` | channel/account | inherits global | Discord TTL override |
| `channels.discord.threadBindings.spawnSubagentSessions` | channel/account | `false` | opt in for `thread: true` spawn auto bind |
### Runtime effect of enable switch
When effective `enabled` is false for a Discord account:
- provider creates a noop thread binding manager for runtime wiring
- no real manager is registered for lookup by account id
- inbound bound thread routing is effectively disabled
- completion routing overrides do not resolve bound thread origins
- `/focus`, `/unfocus`, and thread binding specific operations report unavailable
- `thread: true` spawn path returns actionable error from Discord hook layer
## Flow and behavior
### Spawn with `thread: true`
1. Spawn validates mode and permissions.
2. `subagent_spawning` hook runs.
3. Discord extension checks effective flags:
- thread bindings enabled
- `spawnSubagentSessions` enabled
4. Extension attempts auto bind and thread creation.
5. If bind fails:
- spawn returns error
- provisional child session is deleted
6. If bind succeeds:
- child run starts
- run is registered with spawn mode
### Manual focus and unfocus
- `/focus <target>`
- Discord only
- resolves subagent or session target
- binds current or created thread to target session
- `/unfocus`
- Discord thread only
- unbinds current thread
### Inbound routing
- Discord preflight checks current thread id against thread binding manager.
- If bound, effective session routing uses bound target session key.
- If not bound, normal routing path is used.
### Outbound routing
- Reply delivery checks whether current session has thread bindings.
- Bound sessions deliver to thread via webhook aware path.
- Unbound sessions use normal bot delivery.
### Completion routing
- Core completion flow calls `subagent_delivery_target`.
- Discord extension returns bound thread origin when it can resolve one.
- Core merges hook origin with requester origin and delivers completion.
### Cleanup
Cleanup occurs on:
- completion
- error or timeout completion path
- kill and terminate paths
- TTL expiration
- archived or deleted thread probes
- manual `/unfocus`
Cleanup behavior includes unbind and optional farewell messaging.
## Commands and user UX
| Command | Purpose |
| ---------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------- | --------------- | ------------------------------------------- |
| `/subagents spawn <agentId> <task> [--model] [--thinking]` | spawn subagent; may be thread bound when `thread: true` path is used |
| `/focus <subagent-label | session-key | session-id | session-label>` | manually bind thread to subagent or session |
| `/unfocus` | remove binding from current thread |
| `/agents` | list active agents and binding state |
| `/session ttl <duration | off>` | update TTL for focused thread binding |
Notes:
- `/session ttl` is currently Discord thread focused behavior.
- Thread intro and farewell text are generated by thread binding message helpers.
## Failure handling and safety
- Spawn returns explicit errors when thread binding cannot be prepared.
- Spawn failure after provisional bind attempts best effort unbind and session delete.
- Completion logic prevents duplicate ended hook emission.
- Retry and expiry guards prevent infinite completion announce retry loops.
- Webhook echo suppression avoids unbound webhook messages being reprocessed as inbound turns.
## Module map
### Core orchestration
- `src/agents/subagent-spawn.ts`
- `src/agents/subagent-announce.ts`
- `src/agents/subagent-registry.ts`
- `src/agents/subagent-registry-cleanup.ts`
- `src/agents/subagent-registry-completion.ts`
### Discord runtime
- `src/discord/monitor/provider.ts`
- `src/discord/monitor/thread-bindings.manager.ts`
- `src/discord/monitor/thread-bindings.state.ts`
- `src/discord/monitor/thread-bindings.lifecycle.ts`
- `src/discord/monitor/thread-bindings.messages.ts`
- `src/discord/monitor/message-handler.preflight.ts`
- `src/discord/monitor/message-handler.process.ts`
- `src/discord/monitor/reply-delivery.ts`
### Plugin hooks and extension
- `src/plugins/types.ts`
- `src/plugins/hooks.ts`
- `extensions/discord/src/subagent-hooks.ts`
### Config and schema
- `src/config/types.base.ts`
- `src/config/types.discord.ts`
- `src/config/zod-schema.session.ts`
- `src/config/zod-schema.providers-core.ts`
- `src/config/schema.help.ts`
- `src/config/schema.labels.ts`
## Test coverage highlights
- `extensions/discord/src/subagent-hooks.test.ts`
- `src/discord/monitor/thread-bindings.ttl.test.ts`
- `src/discord/monitor/thread-bindings.shared-state.test.ts`
- `src/discord/monitor/reply-delivery.test.ts`
- `src/discord/monitor/message-handler.preflight.test.ts`
- `src/discord/monitor/message-handler.process.test.ts`
- `src/auto-reply/reply/commands-subagents-focus.test.ts`
- `src/auto-reply/reply/commands-session-ttl.test.ts`
- `src/agents/subagent-registry.steer-restart.test.ts`
- `src/agents/subagent-registry-completion.test.ts`
## Operational summary
- Use `session.threadBindings.enabled` as the global kill switch default.
- Use `channels.discord.threadBindings.enabled` and account overrides for selective enablement.
- Keep `spawnSubagentSessions` opt in for thread auto spawn behavior.
- Use TTL settings for automatic unfocus policy control.
This model keeps subagent lifecycle orchestration generic while giving Discord a full thread bound interaction path.
## Related plan
For channel agnostic SessionBinding architecture and scoped iteration planning, see:
- `docs/experiments/plans/session-binding-channel-agnostic.md`
ACP remains a next step in that plan and is intentionally not implemented in this shipped Discord thread-bound flow.