fix: reply in the Slack DM thread the user wrote in
- chat-sdk-bridge: forward thread.id to the router for DMs so sub-thread context survives into delivery. Previously hardcoded to null, which collapsed every reply to the DM top level. - router: when a DM (is_group=0) is wired as `shared`, don't auto-escalate to per-thread — keep one session for the whole DM and let thread_id flow through to the adapter. - agent-runner poll-loop: defer follow-up messages whose thread_id differs from the active turn's routing. Mixing threads into one streaming turn sent every reply to the first thread because routing is captured at turn start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
exe.dev user
parent
79fd142be4
commit
fdece8047e
@@ -249,13 +249,18 @@ async function processQuery(query: AgentQuery, routing: RoutingContext, config:
|
|||||||
const pollHandle = setInterval(() => {
|
const pollHandle = setInterval(() => {
|
||||||
if (done) return;
|
if (done) return;
|
||||||
|
|
||||||
// Skip system messages (MCP tool responses) and admin commands (need fresh query)
|
// Skip system messages (MCP tool responses) and admin commands (need fresh query).
|
||||||
|
// Also defer messages whose thread_id differs from the active turn's routing
|
||||||
|
// — mixing threads into one streaming turn would send the reply to the wrong
|
||||||
|
// thread because `routing` is captured at turn start. The next turn will pick
|
||||||
|
// them up with fresh routing.
|
||||||
const newMessages = getPendingMessages().filter((m) => {
|
const newMessages = getPendingMessages().filter((m) => {
|
||||||
if (m.kind === 'system') return false;
|
if (m.kind === 'system') return false;
|
||||||
if (m.kind === 'chat' || m.kind === 'chat-sdk') {
|
if (m.kind === 'chat' || m.kind === 'chat-sdk') {
|
||||||
const cmd = categorizeMessage(m);
|
const cmd = categorizeMessage(m);
|
||||||
if (cmd.category === 'admin') return false;
|
if (cmd.category === 'admin') return false;
|
||||||
}
|
}
|
||||||
|
if ((m.thread_id ?? null) !== (routing.threadId ?? null)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (newMessages.length > 0) {
|
if (newMessages.length > 0) {
|
||||||
|
|||||||
@@ -175,11 +175,14 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
|||||||
await thread.subscribe();
|
await thread.subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
// DMs — always forward + subscribe
|
// DMs — always forward + subscribe. Pass thread.id so sub-thread
|
||||||
|
// context carries through to delivery (Slack users can open threads
|
||||||
|
// inside a DM). The router collapses DM sub-threads to one session
|
||||||
|
// (is_group=0 short-circuits the per-thread escalation).
|
||||||
chat.onDirectMessage(async (thread, message) => {
|
chat.onDirectMessage(async (thread, message) => {
|
||||||
const channelId = adapter.channelIdFromThreadId(thread.id);
|
const channelId = adapter.channelIdFromThreadId(thread.id);
|
||||||
log.info('Inbound DM received', { adapter: adapter.name, channelId, sender: (message.author as any)?.fullName ?? (message.author as any)?.userId ?? 'unknown', threadId: thread.id });
|
log.info('Inbound DM received', { adapter: adapter.name, channelId, sender: (message.author as any)?.fullName ?? (message.author as any)?.userId ?? 'unknown', threadId: thread.id });
|
||||||
await setupConfig.onInbound(channelId, null, await messageToInbound(message));
|
await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message));
|
||||||
await thread.subscribe();
|
await thread.subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,12 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
|||||||
// wiring says, because "thread = session" is the first-class model for
|
// wiring says, because "thread = session" is the first-class model for
|
||||||
// threaded platforms. Agent-shared is preserved because it expresses a
|
// threaded platforms. Agent-shared is preserved because it expresses a
|
||||||
// cross-channel intent the adapter can't know about.
|
// cross-channel intent the adapter can't know about.
|
||||||
|
//
|
||||||
|
// Exception: DMs (is_group=0). Sub-threads within a DM are a UX affordance,
|
||||||
|
// not a conversation boundary — treat the whole DM as one session and let
|
||||||
|
// threadId flow through to delivery so replies land in the right sub-thread.
|
||||||
let effectiveSessionMode = match.session_mode;
|
let effectiveSessionMode = match.session_mode;
|
||||||
if (adapter && adapter.supportsThreads && effectiveSessionMode !== 'agent-shared') {
|
if (adapter && adapter.supportsThreads && effectiveSessionMode !== 'agent-shared' && mg.is_group !== 0) {
|
||||||
effectiveSessionMode = 'per-thread';
|
effectiveSessionMode = 'per-thread';
|
||||||
}
|
}
|
||||||
const { session, created } = resolveSession(match.agent_group_id, mg.id, event.threadId, effectiveSessionMode);
|
const { session, created } = resolveSession(match.agent_group_id, mg.id, event.threadId, effectiveSessionMode);
|
||||||
|
|||||||
Reference in New Issue
Block a user