Thread-bound sessions

When thread bindings are enabled for a channel, a spawned sub-agent can get its own new thread. Follow-up user messages in that thread keep routing to the same sub-agent session, while the conversation you spawned it from stays with your agent.

Thread supporting channels

sessions_spawn with thread: true always opens a new child thread; it never hands the current conversation to the worker. Bundled channels that can open one: Discord and Matrix. On channels that would bind the current conversation instead (for example Telegram, iMessage, Feishu, and LINE), thread: true is rejected; spawn with mode: "run" and the result is announced back to the conversation. Use the per-channel threadBindings config keys for enablement, timeouts, and spawnSessions.

Worker bindings created by older versions on the current conversation are ignored: messages there route to your agent again, and the stale binding expires through its normal idle timeout. To hand a conversation to an ACP session deliberately, use /acp spawn --bind here.

Quick flow

Spawn

`sessions_spawn` with `thread: true` (and optionally `mode: "session"`).

Bind

Apus opens a new child thread in the active channel and binds it to that session.

Route follow-ups

Replies and follow-up messages in that thread route to the bound session.

Inspect timeouts

Use `/session idle` to inspect/update inactivity expiry and
`/session max-age` to control the hard cap.

Detach

Use `/session unbind` to detach without closing the agent session.

Manual controls

Command Effect
/session unbind Remove the current conversation binding without closing the agent session
/agents List active runs and binding state (binding:<id>, unbound, or bindings unavailable)
/session idle Inspect/update inactivity expiry for the current binding
/session max-age Inspect/update the maximum age of the current binding

Config switches

See Configuration reference and Slash commands for current adapter details.

Allowlist

agents.entries.*.subagents.allowAgents string[]

List of configured agent ids that can be targeted via explicit agentId (["*"] allows any configured target). Default: only the requester agent. If you set a list and still want the requester to spawn itself with agentId, include the requester id in the list.

agents.defaults.subagents.allowAgents string[]

Default configured target-agent allowlist used when the requester agent does not set its own subagents.allowAgents.

agents.defaults.subagents.requireAgentId boolean

Block sessions_spawn calls that omit agentId (forces explicit profile selection). Per-agent override: agents.entries.*.subagents.requireAgentId.

agents.defaults.subagents.announceTimeoutMs number

Timeout for gateway agent announcement handoff attempts. Once a handoff is accepted, waiting for the parent session's turn does not consume this budget. After execution starts, the requester's normal runtime timeout and cancellation controls apply; the announcement timer does not restart. Values are positive integer milliseconds and are clamped to the platform-safe timer maximum. Queue waits, requester execution, and transient retries can make total delivery time longer than one configured timeout.

If the requester session is sandboxed, sessions_spawn rejects targets that would run unsandboxed.

Discovery

Use agents_list to see which agent ids are currently allowed for sessions_spawn. The response includes each listed agent's effective model and embedded runtime metadata so callers can distinguish Apus, Codex app-server, and other configured native runtimes.

allowAgents entries must point at configured agent ids in agents.entries.*. ["*"] means any configured target agent plus the requester. If an agent config is deleted but its id remains in allowAgents, sessions_spawn rejects that id and agents_list omits it. Run apus doctor --fix to clean stale allowlist entries, or add a minimal agents.entries.* entry when the target should remain spawnable while inheriting defaults.

Auto-archive

If a newer run takes over the same session, the older run stops claiming tabs for cleanup. Cleanup already admitted for a tab still settles against that tab's captured ownership; it does not remove a later registration.

The subagent_ended plugin hook is best-effort. Hook execution or plugin runtime loading failures are logged and do not abort sub-agent cleanup.

Periodic context-engine cleanup continues after the request that started it ends. Best-effort context-engine cleanup failures log the redacted error, cleanup reason, and masked child session key.