mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-10 19:56:00 +00:00
fix(agent): complete thread-session routing for spawn dispatch and system-channel branch
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.
This commit is contained in:
parent
9b6f3d7abc
commit
380309016a
@ -403,7 +403,16 @@ class AgentLoop:
|
||||
session_key: str | None = None,
|
||||
) -> None:
|
||||
"""Update context for all tools that need routing info."""
|
||||
effective_key = UNIFIED_SESSION_KEY if self._unified_session else f"{channel}:{chat_id}"
|
||||
# When the caller threads a thread-scoped session_key (e.g. slack with
|
||||
# reply_in_thread: true), honor it so spawn announces route back to
|
||||
# the originating thread session. Falls back to unified mode or
|
||||
# channel:chat_id for callers that don't have a thread-scoped key.
|
||||
if session_key is not None:
|
||||
effective_key = session_key
|
||||
elif self._unified_session:
|
||||
effective_key = UNIFIED_SESSION_KEY
|
||||
else:
|
||||
effective_key = f"{channel}:{chat_id}"
|
||||
for name in ("message", "spawn", "cron", "my"):
|
||||
if tool := self.tools.get(name):
|
||||
if hasattr(tool, "set_context"):
|
||||
@ -830,7 +839,10 @@ class AgentLoop:
|
||||
msg.chat_id.split(":", 1) if ":" in msg.chat_id else ("cli", msg.chat_id)
|
||||
)
|
||||
logger.info("Processing system message from {}", msg.sender_id)
|
||||
key = f"{channel}:{chat_id}"
|
||||
# Honor session_key_override so subagent announces from threaded
|
||||
# callers route to the originating thread session, not the
|
||||
# channel-level session derived from chat_id.
|
||||
key = msg.session_key_override or f"{channel}:{chat_id}"
|
||||
session = self.sessions.get_or_create(key)
|
||||
if self._restore_runtime_checkpoint(session):
|
||||
self.sessions.save(session)
|
||||
@ -885,11 +897,20 @@ class AgentLoop:
|
||||
options,
|
||||
channel,
|
||||
)
|
||||
# Reconstruct channel-specific metadata from session.key so the
|
||||
# outbound reply lands in the originating thread (not the channel
|
||||
# top-level). The announce InboundMessage carries only
|
||||
# injected_event metadata; we recover thread_ts from the session
|
||||
# key, which slack writes as "slack:<chat_id>:<thread_ts>".
|
||||
outbound_metadata: dict[str, Any] = {}
|
||||
if channel == "slack" and key.startswith("slack:") and key.count(":") >= 2:
|
||||
outbound_metadata["slack"] = {"thread_ts": key.split(":", 2)[2]}
|
||||
return OutboundMessage(
|
||||
channel=channel,
|
||||
chat_id=chat_id,
|
||||
content=content,
|
||||
buttons=buttons,
|
||||
metadata=outbound_metadata,
|
||||
)
|
||||
|
||||
# Extract document text from media at the processing boundary so all
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user