惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
爱范儿
爱范儿
GbyAI
GbyAI
博客园 - 叶小钗
Last Week in AI
Last Week in AI
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
C
Check Point Blog
H
Help Net Security
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
Y
Y Combinator Blog
U
Unit 42
T
Tailwind CSS Blog
MyScale Blog
MyScale Blog
N
Netflix TechBlog - Medium
S
SegmentFault 最新的问题
J
Java Code Geeks
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(slack): preserve assistant DM root thread context (#6...
zozo123 · 2026-05-31 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -179,7 +179,7 @@ export function resolveSlackRoutingContext(params: {

179179
180180

const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";

181181

const replyToMode = resolveSlackReplyToMode(account, chatType);

182-

const threadContext = resolveSlackThreadContext({ message, replyToMode });

182+

const threadContext = resolveSlackThreadContext({ message, replyToMode, isDirectMessage });

183183

const threadTs = threadContext.incomingThreadTs;

184184

const isThreadReply = threadContext.isThreadReply;

185185

// Keep true thread replies thread-scoped, while top-level DMs keep their

Original file line numberDiff line numberDiff line change

@@ -1376,6 +1376,39 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;

13761376

expectMainScopedDmClassification(prepared);

13771377

});

13781378
1379+

it("preserves MessageThreadId for normalized DM assistant thread roots", async () => {

1380+

const cases: Array<{

1381+

name: string;

1382+

message: SlackMessageEvent;

1383+

}> = [

1384+

{

1385+

name: "raw im",

1386+

message: createMainScopedDmMessage({ channel_type: "im", thread_ts: "1.000" }),

1387+

},

1388+

{

1389+

name: "wrong channel_type",

1390+

message: createMainScopedDmMessage({ channel_type: "channel", thread_ts: "1.000" }),

1391+

},

1392+

{

1393+

name: "missing channel_type",

1394+

message: createMainScopedDmMessage({ thread_ts: "1.000" }),

1395+

},

1396+

];

1397+

delete cases[2].message.channel_type;

1398+
1399+

for (const testCase of cases) {

1400+

const prepared = await prepareMessageWith(

1401+

createDmScopeMainSlackCtx(),

1402+

createSlackAccount(),

1403+

testCase.message,

1404+

);

1405+
1406+

expectMainScopedDmClassification(prepared, { includeFromCheck: testCase.name !== "raw im" });

1407+

expect(prepared!.ctxPayload.MessageThreadId).toBe("1.000");

1408+

expect(prepared!.ctxPayload.ReplyToId).toBe("1.000");

1409+

}

1410+

});

1411+
13791412

it("sets MessageThreadId for top-level messages when replyToMode=all", async () => {

13801413

const prepared = await prepareMessageWith(

13811414

createReplyToAllSlackCtx(),

Original file line numberDiff line numberDiff line change

@@ -347,6 +347,7 @@ describe("thread-level session keys", () => {

347347

});

348348
349349

expect(routing.sessionKey).toBe("agent:main:slack:channel:c123");

350+

expect(routing.threadContext.messageThreadId).toBeUndefined();

350351

});

351352
352353

it("does not seed top-level group DM mentions into thread sessions", () => {

@@ -634,4 +635,46 @@ describe("thread-level session keys", () => {

634635

unregisterSessionBindingAdapter({ channel: "slack", accountId: "default", adapter });

635636

}

636637

});

638+
639+

it("preserves distinct MessageThreadIds for concurrent assistant DM roots", () => {

640+

const ctx = buildCtx({ replyToMode: "off", dmScope: "per-channel-peer" });

641+

const account = buildAccount("off");

642+
643+

const first = resolveSlackRoutingContext({

644+

ctx,

645+

account,

646+

message: {

647+

channel: "D456",

648+

channel_type: "channel",

649+

user: "U3",

650+

text: "first assistant root",

651+

ts: "1770408530.000000",

652+

thread_ts: "1770408530.000000",

653+

} as SlackMessageEvent,

654+

isDirectMessage: true,

655+

isGroupDm: false,

656+

isRoom: false,

657+

isRoomish: false,

658+

});

659+

const second = resolveSlackRoutingContext({

660+

ctx,

661+

account,

662+

message: {

663+

channel: "D456",

664+

user: "U3",

665+

text: "second assistant root",

666+

ts: "1770408531.000000",

667+

thread_ts: "1770408531.000000",

668+

} as SlackMessageEvent,

669+

isDirectMessage: true,

670+

isGroupDm: false,

671+

isRoom: false,

672+

isRoomish: false,

673+

});

674+
675+

expect(first.sessionKey).toBe("agent:main:slack:direct:u3");

676+

expect(second.sessionKey).toBe(first.sessionKey);

677+

expect(first.threadContext.messageThreadId).toBe("1770408530.000000");

678+

expect(second.threadContext.messageThreadId).toBe("1770408531.000000");

679+

});

637680

});

Original file line numberDiff line numberDiff line change

@@ -88,6 +88,72 @@ describe("resolveSlackThreadTargets", () => {

8888

expect(context.replyToId).toBe("123");

8989

});

9090
91+

it("sets messageThreadId for DM assistant thread-root messages regardless of replyToMode", () => {

92+

for (const replyToMode of ["off", "first", "batched"] as const) {

93+

const context = resolveSlackThreadContext({

94+

replyToMode,

95+

isDirectMessage: true,

96+

message: {

97+

type: "message",

98+

channel: "D1",

99+

channel_type: "im",

100+

ts: "123",

101+

thread_ts: "123",

102+

},

103+

});

104+
105+

expect(context.isThreadReply).toBe(false);

106+

// thread_ts == ts in a DM: Agents & Assistants root — preserve thread

107+

// context so tool calls (subagent results) thread correctly.

108+

expect(context.messageThreadId).toBe("123");

109+

expect(context.replyToId).toBe("123");

110+

}

111+

});

112+
113+

it("uses normalized direct-message state for DM assistant thread-root messages", () => {

114+

for (const channelType of ["channel", undefined] as const) {

115+

const message = {

116+

type: "message",

117+

channel: "D1",

118+

ts: "123",

119+

thread_ts: "123",

120+

...(channelType ? { channel_type: channelType } : {}),

121+

} as const;

122+
123+

const context = resolveSlackThreadContext({

124+

replyToMode: "off",

125+

isDirectMessage: true,

126+

message,

127+

});

128+
129+

expect(context.isThreadReply).toBe(false);

130+

expect(context.messageThreadId).toBe("123");

131+

expect(context.replyToId).toBe("123");

132+

}

133+

});

134+
135+

it("does not set messageThreadId for channel thread-root messages with non-all replyToMode", () => {

136+

for (const replyToMode of ["off", "first", "batched"] as const) {

137+

const context = resolveSlackThreadContext({

138+

replyToMode,

139+

isDirectMessage: false,

140+

message: {

141+

type: "message",

142+

channel: "C1",

143+

channel_type: "channel",

144+

ts: "123",

145+

thread_ts: "123",

146+

},

147+

});

148+
149+

expect(context.isThreadReply).toBe(false);

150+

// thread_ts == ts in a channel: auto-created top-level thread_ts should

151+

// NOT force threaded mode — only DM assistant threads get the override.

152+

expect(context.messageThreadId).toBeUndefined();

153+

expect(context.replyToId).toBe("123");

154+

}

155+

});

156+
91157

it("prefers thread_ts as messageThreadId for replies", () => {

92158

const context = resolveSlackThreadContext({

93159

replyToMode: "off",

Original file line numberDiff line numberDiff line change

@@ -12,6 +12,7 @@ type SlackThreadContext = {

1212

export function resolveSlackThreadContext(params: {

1313

message: SlackMessageEvent | SlackAppMentionEvent;

1414

replyToMode: ReplyToMode;

15+

isDirectMessage?: boolean;

1516

}): SlackThreadContext {

1617

const incomingThreadTs = params.message.thread_ts;

1718

const eventTs = params.message.event_ts;

@@ -20,11 +21,17 @@ export function resolveSlackThreadContext(params: {

2021

const isThreadReply =

2122

hasThreadTs && (incomingThreadTs !== messageTs || Boolean(params.message.parent_user_id));

2223

const replyToId = incomingThreadTs ?? messageTs;

23-

const messageThreadId = isThreadReply

24-

? incomingThreadTs

25-

: params.replyToMode === "all"

26-

? messageTs

27-

: undefined;

24+

// Preserve thread context for Slack Agents & Assistants DM root messages

25+

// where thread_ts == ts. Non-DM self-thread roots must stay unset because

26+

// downstream tool threading treats MessageThreadId as an explicit thread

27+

// target and overrides replyToMode to "all".

28+

const isAssistantDmThreadRoot = hasThreadTs && !isThreadReply && params.isDirectMessage === true;

29+

const messageThreadId =

30+

isThreadReply || isAssistantDmThreadRoot

31+

? incomingThreadTs

32+

: params.replyToMode === "all"

33+

? messageTs

34+

: undefined;

2835

return {

2936

incomingThreadTs,

3037

messageTs,