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

推荐订阅源

月光博客
月光博客
D
Docker
腾讯CDC
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
Martin Fowler
Martin Fowler
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
IT之家
IT之家
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
博客园 - 【当耐特】

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(sessions): reset stale per-channel origin fields on c...
ZengWen-DT · 2026-06-21 · via Recent Commits to openclaw:main
1+

// Session origin must drop the prior channel's identity when a dmScope:"main" session moves

2+

// across providers, so channel-keyed fields do not reference a now-inactive channel.

3+

import { describe, expect, it } from "vitest";

4+

import type { MsgContext } from "../../auto-reply/templating.js";

5+

import { buildChannelInboundEventContext } from "../../channels/inbound-event/context.js";

6+

import { deriveSessionMetaPatch } from "./metadata.js";

7+

import type { SessionEntry } from "./types.js";

8+9+

const sessionKey = "agent:user";

10+11+

function applyOrigin(existing: SessionEntry | undefined, ctx: Partial<MsgContext>): SessionEntry {

12+

const patch = deriveSessionMetaPatch({

13+

ctx: ctx as MsgContext,

14+

sessionKey,

15+

existing,

16+

});

17+

return { ...existing, ...patch } as SessionEntry;

18+

}

19+20+

const slackTurn = {

21+

Provider: "slack",

22+

Surface: "slack",

23+

ChatType: "direct",

24+

From: "slack:U0001",

25+

To: "slack:D111SLACK",

26+

NativeChannelId: "D111SLACK",

27+

NativeDirectUserId: "U0001",

28+

AccountId: "slack-team-1",

29+

MessageThreadId: "1700000000.000100",

30+

} satisfies Partial<MsgContext>;

31+32+

const telegramTurn = {

33+

Provider: "telegram",

34+

Surface: "telegram",

35+

ChatType: "direct",

36+

From: "telegram:42",

37+

To: "telegram:42",

38+

AccountId: "telegram-bot-1",

39+

} satisfies Partial<MsgContext>;

40+41+

describe("session origin across a channel switch", () => {

42+

it("clears stale channel-keyed fields when provider changes and the new turn omits them", () => {

43+

const afterSlack = applyOrigin(undefined, slackTurn);

44+

expect(afterSlack.origin?.nativeChannelId).toBe("D111SLACK");

45+

expect(afterSlack.origin?.threadId).toBe("1700000000.000100");

46+47+

const afterTelegram = applyOrigin(afterSlack, telegramTurn);

48+49+

// Provider/surface flip to Telegram, and the Slack-only identity must not survive.

50+

expect(afterTelegram.origin?.provider).toBe("telegram");

51+

expect(afterTelegram.origin?.surface).toBe("telegram");

52+

expect(afterTelegram.origin?.accountId).toBe("telegram-bot-1");

53+

expect(afterTelegram.origin?.nativeChannelId).toBeUndefined();

54+

expect(afterTelegram.origin?.nativeDirectUserId).toBeUndefined();

55+

expect(afterTelegram.origin?.threadId).toBeUndefined();

56+

});

57+58+

it("does not re-stamp the prior channel id on subsequent same-channel turns", () => {

59+

const afterSlack = applyOrigin(undefined, slackTurn);

60+

const afterTelegram = applyOrigin(afterSlack, telegramTurn);

61+

const afterTelegramAgain = applyOrigin(afterTelegram, telegramTurn);

62+63+

expect(afterTelegramAgain.origin?.provider).toBe("telegram");

64+

expect(afterTelegramAgain.origin?.nativeChannelId).toBeUndefined();

65+

expect(afterTelegramAgain.origin?.threadId).toBeUndefined();

66+

});

67+68+

it("clears stale account id when provider changes and the new turn omits it", () => {

69+

const afterSlack = applyOrigin(undefined, slackTurn);

70+

const telegramWithoutAccount = {

71+

Provider: "telegram",

72+

Surface: "telegram",

73+

ChatType: "direct",

74+

From: "telegram:42",

75+

To: "telegram:42",

76+

} satisfies Partial<MsgContext>;

77+78+

const afterTelegram = applyOrigin(afterSlack, telegramWithoutAccount);

79+80+

expect(afterTelegram.origin?.provider).toBe("telegram");

81+

expect(afterTelegram.origin?.accountId).toBeUndefined();

82+

});

83+84+

it("adopts the new channel's identity when the new turn supplies it", () => {

85+

const afterSlack = applyOrigin(undefined, slackTurn);

86+

const telegramWithChannel = {

87+

...telegramTurn,

88+

NativeChannelId: "C222TG",

89+

MessageThreadId: 555,

90+

} satisfies Partial<MsgContext>;

91+92+

const afterTelegram = applyOrigin(afterSlack, telegramWithChannel);

93+

expect(afterTelegram.origin?.nativeChannelId).toBe("C222TG");

94+

expect(afterTelegram.origin?.threadId).toBe(555);

95+

});

96+97+

it("preserves sparse channel metadata across turns on the same provider", () => {

98+

const afterSlack = applyOrigin(undefined, slackTurn);

99+

const slackFollowUp = {

100+

Provider: "slack",

101+

Surface: "slack",

102+

ChatType: "direct",

103+

From: "slack:U0001",

104+

To: "slack:D111SLACK",

105+

AccountId: "slack-team-1",

106+

} satisfies Partial<MsgContext>;

107+108+

const afterFollowUp = applyOrigin(afterSlack, slackFollowUp);

109+

// Same provider: the established channel id and thread are retained when omitted.

110+

expect(afterFollowUp.origin?.nativeChannelId).toBe("D111SLACK");

111+

expect(afterFollowUp.origin?.threadId).toBe("1700000000.000100");

112+

});

113+114+

it("clears stale channel-keyed fields when the account changes and the new turn omits them", () => {

115+

const afterSlack = applyOrigin(undefined, slackTurn);

116+

const slackOtherAccount = {

117+

Provider: "slack",

118+

Surface: "slack",

119+

ChatType: "direct",

120+

From: "slack:U0002",

121+

To: "slack:D222SLACK",

122+

AccountId: "slack-team-2",

123+

} satisfies Partial<MsgContext>;

124+125+

const afterAccountSwitch = applyOrigin(afterSlack, slackOtherAccount);

126+127+

expect(afterAccountSwitch.origin?.provider).toBe("slack");

128+

expect(afterAccountSwitch.origin?.accountId).toBe("slack-team-2");

129+

expect(afterAccountSwitch.origin?.nativeChannelId).toBeUndefined();

130+

expect(afterAccountSwitch.origin?.nativeDirectUserId).toBeUndefined();

131+

expect(afterAccountSwitch.origin?.threadId).toBeUndefined();

132+

});

133+134+

it("preserves sparse existing channel metadata when optional identity fields are first populated", () => {

135+

const existing = {

136+

sessionId: "session-1",

137+

updatedAt: 1,

138+

origin: {

139+

provider: "slack",

140+

nativeChannelId: "D111SLACK",

141+

threadId: "1700000000.000100",

142+

},

143+

} satisfies SessionEntry;

144+

const slackFollowUp = {

145+

Provider: "slack",

146+

Surface: "slack",

147+

ChatType: "direct",

148+

From: "slack:U0001",

149+

To: "slack:D111SLACK",

150+

AccountId: "slack-team-1",

151+

} satisfies Partial<MsgContext>;

152+153+

const afterFollowUp = applyOrigin(existing, slackFollowUp);

154+155+

expect(afterFollowUp.origin?.surface).toBe("slack");

156+

expect(afterFollowUp.origin?.accountId).toBe("slack-team-1");

157+

expect(afterFollowUp.origin?.nativeChannelId).toBe("D111SLACK");

158+

expect(afterFollowUp.origin?.threadId).toBe("1700000000.000100");

159+

});

160+

});

161+162+

// Drive the production inbound-event context builder so the bug premise itself is proven, not

163+

// assumed: a Slack DM turn populates ctx.NativeChannelId (from conversation.nativeChannelId), a

164+

// Telegram DM turn omits it, and the same derivation path runs that real context.

165+

function buildDirectTurn(opts: {

166+

provider: string;

167+

from: string;

168+

to: string;

169+

accountId: string;

170+

conversationId: string;

171+

nativeChannelId?: string;

172+

}): MsgContext {

173+

return buildChannelInboundEventContext({

174+

channel: opts.provider,

175+

provider: opts.provider,

176+

surface: opts.provider,

177+

accountId: opts.accountId,

178+

messageId: "m-1",

179+

from: opts.from,

180+

sender: { id: opts.from },

181+

conversation: {

182+

kind: "direct",

183+

id: opts.conversationId,

184+

...(opts.nativeChannelId ? { nativeChannelId: opts.nativeChannelId } : {}),

185+

},

186+

route: { agentId: "main", accountId: opts.accountId, routeSessionKey: sessionKey },

187+

reply: { to: opts.to },

188+

message: { rawBody: "hi" },

189+

}) as MsgContext;

190+

}

191+192+

describe("session origin across a channel switch (real inbound-event context builder)", () => {

193+

const slackCtx = buildDirectTurn({

194+

provider: "slack",

195+

from: "slack:U0001",

196+

to: "slack:D111SLACK",

197+

accountId: "slack-team-1",

198+

conversationId: "D111SLACK",

199+

nativeChannelId: "D111SLACK",

200+

});

201+

const telegramCtx = buildDirectTurn({

202+

provider: "telegram",

203+

from: "telegram:42",

204+

to: "telegram:42",

205+

accountId: "telegram-bot-1",

206+

conversationId: "42",

207+

});

208+209+

it("confirms the premise: Slack DM context supplies NativeChannelId, Telegram DM context omits it", () => {

210+

expect(slackCtx.NativeChannelId).toBe("D111SLACK");

211+

expect(telegramCtx.NativeChannelId).toBeUndefined();

212+

});

213+214+

it("resets the stale Slack channel id after a real-context Slack->Telegram switch", () => {

215+

const afterSlack = applyOrigin(undefined, slackCtx);

216+

expect(afterSlack.origin?.nativeChannelId).toBe("D111SLACK");

217+218+

const afterTelegram = applyOrigin(afterSlack, telegramCtx);

219+

expect(afterTelegram.origin?.provider).toBe("telegram");

220+

expect(afterTelegram.origin?.nativeChannelId).toBeUndefined();

221+

});

222+

});