Skip to content

Commit c02103a

Browse files
committed
fix(slack): keep outbound mirrors on thread session
1 parent 643410c commit c02103a

6 files changed

Lines changed: 54 additions & 2 deletions

File tree

extensions/slack/src/channel.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,49 @@ describe("slackPlugin status", () => {
475475
});
476476
});
477477

478+
it("uses the current Slack thread session before inherited child reply ids", async () => {
479+
const resolveRoute = slackPlugin.messaging?.resolveOutboundSessionRoute;
480+
if (!resolveRoute) {
481+
throw new Error("slack messaging.resolveOutboundSessionRoute unavailable");
482+
}
483+
484+
const route = await resolveRoute({
485+
cfg: {} as OpenClawConfig,
486+
agentId: "main",
487+
target: "channel:C1",
488+
currentSessionKey: "agent:main:slack:channel:C1:thread:1712345678.123456",
489+
replyToId: "1712345688.654321",
490+
});
491+
492+
expectRecordFields(route, "Slack route", {
493+
sessionKey: "agent:main:slack:channel:c1:thread:1712345678.123456",
494+
baseSessionKey: "agent:main:slack:channel:c1",
495+
threadId: "1712345678.123456",
496+
});
497+
});
498+
499+
it("keeps explicit Slack reply ids ahead of the current thread session", async () => {
500+
const resolveRoute = slackPlugin.messaging?.resolveOutboundSessionRoute;
501+
if (!resolveRoute) {
502+
throw new Error("slack messaging.resolveOutboundSessionRoute unavailable");
503+
}
504+
505+
const route = await resolveRoute({
506+
cfg: {} as OpenClawConfig,
507+
agentId: "main",
508+
target: "channel:C1",
509+
currentSessionKey: "agent:main:slack:channel:C1:thread:1712345678.123456",
510+
replyToId: "1712345688.654321",
511+
replyToIsExplicit: true,
512+
});
513+
514+
expectRecordFields(route, "Slack route", {
515+
sessionKey: "agent:main:slack:channel:c1:thread:1712345688.654321",
516+
baseSessionKey: "agent:main:slack:channel:c1",
517+
threadId: "1712345688.654321",
518+
});
519+
});
520+
478521
it("canonicalizes bare Slack IM channel targets to direct user session routes", async () => {
479522
const resolveRoute = slackPlugin.messaging?.resolveOutboundSessionRoute;
480523
if (!resolveRoute) {

extensions/slack/src/channel.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ async function resolveSlackOutboundSessionRoute(params: {
312312
accountId?: string | null;
313313
target: string;
314314
replyToId?: string | null;
315+
replyToIsExplicit?: boolean;
315316
threadId?: string | number | null;
316317
currentSessionKey?: string | null;
317318
}) {
@@ -373,6 +374,9 @@ async function resolveSlackOutboundSessionRoute(params: {
373374
replyToId: params.replyToId,
374375
threadId: params.threadId,
375376
currentSessionKey: params.currentSessionKey,
377+
precedence: params.replyToIsExplicit
378+
? ["replyToId", "threadId", "currentSession"]
379+
: ["currentSession", "threadId", "replyToId"],
376380
canRecoverCurrentThread: () =>
377381
shouldRecoverSlackThreadFromCurrentSession({
378382
cfg: params.cfg,
@@ -661,8 +665,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
661665
}
662666
return resolveTargetsWithOptionalToken({
663667
token:
664-
normalizeOptionalString(account.userToken) ??
665-
normalizeOptionalString(account.botToken),
668+
normalizeOptionalString(account.userToken) ?? normalizeOptionalString(account.botToken),
666669
inputs,
667670
missingTokenNote: "missing Slack token",
668671
resolveWithToken: async ({ token, inputs: inputsLocal }) =>

src/channels/plugins/types.core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,8 @@ export type ChannelMessagingAdapter = {
647647
source: "normalized" | "directory";
648648
};
649649
replyToId?: string | null;
650+
/** True when replyToId came from an explicit payload target or reply tag. */
651+
replyToIsExplicit?: boolean;
650652
threadId?: string | number | null;
651653
}) => ChannelOutboundSessionRoute | Promise<ChannelOutboundSessionRoute | null> | null;
652654
};

src/infra/outbound/message-action-runner.threading.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,15 @@ describe("message action threading helpers", () => {
122122
replyToId: threadId == null ? threadId : String(threadId),
123123
threadId: threadId ?? null,
124124
}),
125+
replyToIsExplicit: false,
125126
resolveOutboundSessionRoute,
126127
ensureOutboundSessionEntry,
127128
});
128129

129130
expect(resolveOutboundSessionRoute).toHaveBeenCalledOnce();
130131
expect(firstMockArg(resolveOutboundSessionRoute)).toMatchObject({
131132
replyToId: "root-42",
133+
replyToIsExplicit: false,
132134
threadId: "root-42",
133135
});
134136
});

src/infra/outbound/message-action-threading.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export async function prepareOutboundMirrorRoute(params: {
211211
currentSessionKey: params.currentSessionKey,
212212
resolvedTarget: params.resolvedTarget,
213213
replyToId,
214+
replyToIsExplicit: params.replyToIsExplicit,
214215
threadId: resolvedThreadId,
215216
})
216217
: null;

src/infra/outbound/outbound-session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type ResolveOutboundSessionRouteParams = {
3939
currentSessionKey?: string;
4040
resolvedTarget?: ResolvedMessagingTarget;
4141
replyToId?: string | null;
42+
replyToIsExplicit?: boolean;
4243
threadId?: string | number | null;
4344
};
4445

0 commit comments

Comments
 (0)