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>
This commit is contained in:
@@ -1292,3 +1292,62 @@ describe("persistSessionUsageUpdate", () => {
|
||||
expect(stored[sessionKey].totalTokensFresh).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initSessionState stale threadId fallback", () => {
|
||||
it("does not inherit lastThreadId from a previous thread interaction in non-thread sessions", async () => {
|
||||
const storePath = await createStorePath("stale-thread-");
|
||||
const cfg = { session: { store: storePath } } as OpenClawConfig;
|
||||
|
||||
// First interaction: inside a DM topic (thread session)
|
||||
const threadResult = await initSessionState({
|
||||
ctx: {
|
||||
Body: "hello from topic",
|
||||
SessionKey: "agent:main:main:thread:42",
|
||||
MessageThreadId: 42,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
expect(threadResult.sessionEntry.lastThreadId).toBe(42);
|
||||
|
||||
// Second interaction: plain DM (non-thread session), same store
|
||||
// The main session should NOT inherit threadId=42
|
||||
const mainResult = await initSessionState({
|
||||
ctx: {
|
||||
Body: "hello from DM",
|
||||
SessionKey: "agent:main:main",
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
expect(mainResult.sessionEntry.lastThreadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves lastThreadId within the same thread session", async () => {
|
||||
const storePath = await createStorePath("preserve-thread-");
|
||||
const cfg = { session: { store: storePath } } as OpenClawConfig;
|
||||
|
||||
// First message in thread
|
||||
await initSessionState({
|
||||
ctx: {
|
||||
Body: "first",
|
||||
SessionKey: "agent:main:main:thread:99",
|
||||
MessageThreadId: 99,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
// Second message in same thread (MessageThreadId still present)
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: "second",
|
||||
SessionKey: "agent:main:main:thread:99",
|
||||
MessageThreadId: 99,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
expect(result.sessionEntry.lastThreadId).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,7 +258,10 @@ export async function initSessionState(params: {
|
||||
const lastChannelRaw = (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
|
||||
const lastToRaw = ctx.OriginatingTo || ctx.To || baseEntry?.lastTo;
|
||||
const lastAccountIdRaw = ctx.AccountId || baseEntry?.lastAccountId;
|
||||
const lastThreadIdRaw = ctx.MessageThreadId || baseEntry?.lastThreadId;
|
||||
// Only fall back to persisted threadId for thread sessions. Non-thread
|
||||
// sessions (e.g. DM without topics) must not inherit a stale threadId from a
|
||||
// previous interaction that happened inside a topic/thread.
|
||||
const lastThreadIdRaw = ctx.MessageThreadId || (isThread ? baseEntry?.lastThreadId : undefined);
|
||||
const deliveryFields = normalizeSessionDeliveryFields({
|
||||
deliveryContext: {
|
||||
channel: lastChannelRaw,
|
||||
|
||||
Reference in New Issue
Block a user