Skip to content

Commit 5f821ed

Browse files
j2h4uclaude
authored andcommitted
fix(session): prevent stale threadId leaking into non-thread sessions
When a user interacts with the bot inside a DM topic (thread), the session persists `lastThreadId`. If the user later sends a message from the main DM (no topic), `ctx.MessageThreadId` is undefined and the `||` fallback picks up the stale persisted value — causing the bot to reply into the old topic instead of the main conversation. Only fall back to `baseEntry.lastThreadId` for thread sessions where the fallback is meaningful (e.g. consecutive messages in the same thread). Non-thread sessions now correctly leave threadId unset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 01b37f1 commit 5f821ed

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

src/auto-reply/reply/session.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,3 +1292,62 @@ describe("persistSessionUsageUpdate", () => {
12921292
expect(stored[sessionKey].totalTokensFresh).toBe(true);
12931293
});
12941294
});
1295+
1296+
describe("initSessionState stale threadId fallback", () => {
1297+
it("does not inherit lastThreadId from a previous thread interaction in non-thread sessions", async () => {
1298+
const storePath = await createStorePath("stale-thread-");
1299+
const cfg = { session: { store: storePath } } as OpenClawConfig;
1300+
1301+
// First interaction: inside a DM topic (thread session)
1302+
const threadResult = await initSessionState({
1303+
ctx: {
1304+
Body: "hello from topic",
1305+
SessionKey: "agent:main:main:thread:42",
1306+
MessageThreadId: 42,
1307+
},
1308+
cfg,
1309+
commandAuthorized: true,
1310+
});
1311+
expect(threadResult.sessionEntry.lastThreadId).toBe(42);
1312+
1313+
// Second interaction: plain DM (non-thread session), same store
1314+
// The main session should NOT inherit threadId=42
1315+
const mainResult = await initSessionState({
1316+
ctx: {
1317+
Body: "hello from DM",
1318+
SessionKey: "agent:main:main",
1319+
},
1320+
cfg,
1321+
commandAuthorized: true,
1322+
});
1323+
expect(mainResult.sessionEntry.lastThreadId).toBeUndefined();
1324+
});
1325+
1326+
it("preserves lastThreadId within the same thread session", async () => {
1327+
const storePath = await createStorePath("preserve-thread-");
1328+
const cfg = { session: { store: storePath } } as OpenClawConfig;
1329+
1330+
// First message in thread
1331+
await initSessionState({
1332+
ctx: {
1333+
Body: "first",
1334+
SessionKey: "agent:main:main:thread:99",
1335+
MessageThreadId: 99,
1336+
},
1337+
cfg,
1338+
commandAuthorized: true,
1339+
});
1340+
1341+
// Second message in same thread (MessageThreadId still present)
1342+
const result = await initSessionState({
1343+
ctx: {
1344+
Body: "second",
1345+
SessionKey: "agent:main:main:thread:99",
1346+
MessageThreadId: 99,
1347+
},
1348+
cfg,
1349+
commandAuthorized: true,
1350+
});
1351+
expect(result.sessionEntry.lastThreadId).toBe(99);
1352+
});
1353+
});

src/auto-reply/reply/session.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ export async function initSessionState(params: {
258258
const lastChannelRaw = (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
259259
const lastToRaw = ctx.OriginatingTo || ctx.To || baseEntry?.lastTo;
260260
const lastAccountIdRaw = ctx.AccountId || baseEntry?.lastAccountId;
261-
const lastThreadIdRaw = ctx.MessageThreadId || baseEntry?.lastThreadId;
261+
// Only fall back to persisted threadId for thread sessions. Non-thread
262+
// sessions (e.g. DM without topics) must not inherit a stale threadId from a
263+
// previous interaction that happened inside a topic/thread.
264+
const lastThreadIdRaw = ctx.MessageThreadId || (isThread ? baseEntry?.lastThreadId : undefined);
262265
const deliveryFields = normalizeSessionDeliveryFields({
263266
deliveryContext: {
264267
channel: lastChannelRaw,

0 commit comments

Comments
 (0)