Builds on PR #3463 (commit 038a140), which introduced metadata and
session_key parameters through _LoopHook and _set_tool_context for the
cron and message tools. Three downstream gaps remained:
1. _set_tool_context's body still computes effective_key from
channel:chat_id and passes that to spawn, even when the caller
provides a thread-scoped session_key. The new parameter is wired in
for cron/message but spawn dispatch ignores it. Result: subagent
announces from threaded callers carry a channel-only
session_key_override, dropping thread_ts.
2. _process_message's system-channel branch loads the session via
key = f"{channel}:{chat_id}", ignoring msg.session_key_override.
So even when the announce InboundMessage carries the right override
(after fix 1), the consumer side discards it and routes to the
channel-level session.
3. The OutboundMessage returned from the system-channel branch has no
metadata, so slack's outbound dispatcher has no thread_ts to use and
posts the LLM's reply to the channel top-level rather than the
originating thread.
This change closes all three gaps with three small edits in loop.py.
Behavior change:
- Slack channels with reply_in_thread: true: subagent announces and
follow-up replies now arrive in the originating thread session
instead of leaking into the channel-level session.
- Other channels constructing thread-scoped session keys (matrix
threads, telegram thread mode, etc.): the session-loading and
effective-key fixes apply identically since they're platform-agnostic.
The outbound thread_ts reconstruction is slack-specific by virtue of
the session-key format slack uses; other channels would benefit from
the same pattern but are out of scope for this PR.
- Unified session mode: no change. Falls back to UNIFIED_SESSION_KEY
when session_key is not provided.
- CLI / non-channel callers: no change. They don't pass session_key
and the fallback to f"{channel}:{chat_id}" matches prior behavior.
Reproducer (slack with reply_in_thread: true):
1. From a slack thread, send a message that triggers a subagent spawn.
2. Before fix: announce lands in slack:<channel>.jsonl session,
parent agent in the thread never sees the completion event,
eventual reply (if any) posts to the channel top-level, not the
thread.
3. After fix: announce lands in slack:<channel>:<thread_ts>.jsonl,
parent agent in the thread responds within seconds, reply posts in
the thread.