Skip to content

Commit 6a1eedf

Browse files
不做了睡大觉Usersteipete
authored
fix: deliver subagent completion announces to Slack without invalid thread_ts (#31105)
* fix(subagent): avoid invalid Slack thread_ts for bound completion announces * build: regenerate host env security policy swift --------- Co-authored-by: User <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent ed86252 commit 6a1eedf

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

src/agents/subagent-announce.format.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,67 @@ describe("subagent announce formatting", () => {
849849
}
850850
});
851851

852+
it("does not force Slack threadId from bound conversation id", async () => {
853+
sendSpy.mockClear();
854+
agentSpy.mockClear();
855+
sessionStore = {
856+
"agent:main:subagent:test": {
857+
sessionId: "child-session-slack-bound",
858+
},
859+
"agent:main:main": {
860+
sessionId: "requester-session-slack-bound",
861+
},
862+
};
863+
chatHistoryMock.mockResolvedValueOnce({
864+
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
865+
});
866+
registerSessionBindingAdapter({
867+
channel: "slack",
868+
accountId: "acct-1",
869+
listBySession: (targetSessionKey: string) =>
870+
targetSessionKey === "agent:main:subagent:test"
871+
? [
872+
{
873+
bindingId: "slack:acct-1:C123",
874+
targetSessionKey,
875+
targetKind: "subagent",
876+
conversation: {
877+
channel: "slack",
878+
accountId: "acct-1",
879+
conversationId: "C123",
880+
},
881+
status: "active",
882+
boundAt: Date.now(),
883+
},
884+
]
885+
: [],
886+
resolveByConversation: () => null,
887+
});
888+
889+
const didAnnounce = await runSubagentAnnounceFlow({
890+
childSessionKey: "agent:main:subagent:test",
891+
childRunId: "run-direct-slack-bound",
892+
requesterSessionKey: "agent:main:main",
893+
requesterDisplayKey: "main",
894+
requesterOrigin: {
895+
channel: "slack",
896+
to: "channel:C123",
897+
accountId: "acct-1",
898+
},
899+
...defaultOutcomeAnnounce,
900+
expectsCompletionMessage: true,
901+
spawnMode: "session",
902+
});
903+
904+
expect(didAnnounce).toBe(true);
905+
expect(sendSpy).toHaveBeenCalledTimes(1);
906+
expect(agentSpy).not.toHaveBeenCalled();
907+
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
908+
expect(call?.params?.channel).toBe("slack");
909+
expect(call?.params?.to).toBe("channel:C123");
910+
expect(call?.params?.threadId).toBeUndefined();
911+
});
912+
852913
it("routes manual completion direct-send for telegram forum topics", async () => {
853914
sendSpy.mockClear();
854915
agentSpy.mockClear();

src/agents/subagent-announce.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,14 @@ async function resolveSubagentCompletionOrigin(params: {
517517
channel: route.binding.conversation.channel,
518518
accountId: route.binding.conversation.accountId,
519519
to: `channel:${route.binding.conversation.conversationId}`,
520-
threadId: route.binding.conversation.conversationId,
520+
// `conversationId` identifies the target conversation (channel/DM/thread),
521+
// but it is not always a thread identifier. Passing it as `threadId` breaks
522+
// Slack DM/top-level delivery by forcing an invalid thread_ts. Preserve only
523+
// explicit requester thread hints for channels that actually use threading.
524+
threadId:
525+
requesterOrigin?.threadId != null && requesterOrigin.threadId !== ""
526+
? String(requesterOrigin.threadId)
527+
: undefined,
521528
};
522529
return {
523530
// Bound target is authoritative; requester hints fill only missing fields.

0 commit comments

Comments
 (0)