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:
j2h4u
2026-02-17 01:08:45 +05:00
committed by Peter Steinberger
parent 01b37f1d32
commit 5f821ed067
2 changed files with 63 additions and 1 deletions

View File

@@ -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);
});
});

View File

@@ -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,