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 <user@example.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
不做了睡大觉
2026-03-02 09:11:08 +08:00
committed by GitHub
parent ed86252aa5
commit 6a1eedf10b
2 changed files with 69 additions and 1 deletions

View File

@@ -849,6 +849,67 @@ describe("subagent announce formatting", () => {
}
});
it("does not force Slack threadId from bound conversation id", async () => {
sendSpy.mockClear();
agentSpy.mockClear();
sessionStore = {
"agent:main:subagent:test": {
sessionId: "child-session-slack-bound",
},
"agent:main:main": {
sessionId: "requester-session-slack-bound",
},
};
chatHistoryMock.mockResolvedValueOnce({
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
});
registerSessionBindingAdapter({
channel: "slack",
accountId: "acct-1",
listBySession: (targetSessionKey: string) =>
targetSessionKey === "agent:main:subagent:test"
? [
{
bindingId: "slack:acct-1:C123",
targetSessionKey,
targetKind: "subagent",
conversation: {
channel: "slack",
accountId: "acct-1",
conversationId: "C123",
},
status: "active",
boundAt: Date.now(),
},
]
: [],
resolveByConversation: () => null,
});
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-slack-bound",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: {
channel: "slack",
to: "channel:C123",
accountId: "acct-1",
},
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
spawnMode: "session",
});
expect(didAnnounce).toBe(true);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).not.toHaveBeenCalled();
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.channel).toBe("slack");
expect(call?.params?.to).toBe("channel:C123");
expect(call?.params?.threadId).toBeUndefined();
});
it("routes manual completion direct-send for telegram forum topics", async () => {
sendSpy.mockClear();
agentSpy.mockClear();

View File

@@ -517,7 +517,14 @@ async function resolveSubagentCompletionOrigin(params: {
channel: route.binding.conversation.channel,
accountId: route.binding.conversation.accountId,
to: `channel:${route.binding.conversation.conversationId}`,
threadId: route.binding.conversation.conversationId,
// `conversationId` identifies the target conversation (channel/DM/thread),
// but it is not always a thread identifier. Passing it as `threadId` breaks
// Slack DM/top-level delivery by forcing an invalid thread_ts. Preserve only
// explicit requester thread hints for channels that actually use threading.
threadId:
requesterOrigin?.threadId != null && requesterOrigin.threadId !== ""
? String(requesterOrigin.threadId)
: undefined,
};
return {
// Bound target is authoritative; requester hints fill only missing fields.