* 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>
339 lines
12 KiB
Markdown
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.
|