fix(session): clear stale thread route on system events

This commit is contained in:
Mariano Belinky
2026-04-13 19:55:09 +02:00
parent a372e4a152
commit fbdbd998d3
2 changed files with 96 additions and 0 deletions

View File

@@ -2520,6 +2520,76 @@ describe("initSessionState dmScope delivery migration", () => {
});
describe("initSessionState internal channel routing preservation", () => {
it("clears stale thread routing on non-thread system-event sessions", async () => {
const storePath = await createStorePath("system-event-clears-stale-thread-");
const sessionKey = "agent:main:mattermost:channel:chan1";
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: "sess-system-event-stale-thread",
updatedAt: Date.now(),
lastChannel: "mattermost",
lastTo: "channel:CHAN1",
lastAccountId: "default",
lastThreadId: "stale-root",
deliveryContext: {
channel: "mattermost",
to: "channel:CHAN1",
accountId: "default",
threadId: "stale-root",
},
origin: {
provider: "mattermost",
to: "channel:CHAN1",
accountId: "default",
threadId: "stale-root",
},
},
});
const cfg = { session: { store: storePath } } as OpenClawConfig;
const result = await initSessionState({
ctx: {
Body: "heartbeat tick",
SessionKey: sessionKey,
Provider: "heartbeat",
From: "heartbeat",
To: "heartbeat",
},
cfg,
commandAuthorized: true,
});
expect(result.sessionEntry.lastChannel).toBe("mattermost");
expect(result.sessionEntry.lastTo).toBe("channel:CHAN1");
expect(result.sessionEntry.lastThreadId).toBeUndefined();
expect(result.sessionEntry.deliveryContext).toEqual({
channel: "mattermost",
to: "channel:CHAN1",
accountId: "default",
});
expect(result.sessionEntry.origin).toEqual({
provider: "mattermost",
to: "channel:CHAN1",
accountId: "default",
});
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
SessionEntry
>;
expect(persisted[sessionKey]?.lastThreadId).toBeUndefined();
expect(persisted[sessionKey]?.deliveryContext).toEqual({
channel: "mattermost",
to: "channel:CHAN1",
accountId: "default",
});
expect(persisted[sessionKey]?.origin).toEqual({
provider: "mattermost",
to: "channel:CHAN1",
accountId: "default",
});
});
it("does not synthesize heartbeat routing on a session with no external route", async () => {
const storePath = await createStorePath("system-event-no-route-");
const sessionKey = "agent:main:main";

View File

@@ -67,6 +67,24 @@ function loadSessionArchiveRuntime() {
return sessionArchiveRuntimePromise;
}
function stripThreadIdFromDeliveryContext(
context: SessionEntry["deliveryContext"],
): SessionEntry["deliveryContext"] {
if (!context || context.threadId == null || context.threadId === "") {
return context;
}
const { threadId: _threadId, ...rest } = context;
return Object.keys(rest).length > 0 ? rest : undefined;
}
function stripThreadIdFromOrigin(origin: SessionEntry["origin"]): SessionEntry["origin"] {
if (!origin || origin.threadId == null || origin.threadId === "") {
return origin;
}
const { threadId: _threadId, ...rest } = origin;
return Object.keys(rest).length > 0 ? rest : undefined;
}
function resolveExplicitSessionEndReason(
matchedResetTriggerLower?: string,
): PluginHookSessionEndReason {
@@ -607,6 +625,14 @@ export async function initSessionState(params: {
if (metaPatch) {
sessionEntry = { ...sessionEntry, ...metaPatch };
}
if (isSystemEvent && !isThread) {
sessionEntry = {
...sessionEntry,
lastThreadId: undefined,
deliveryContext: stripThreadIdFromDeliveryContext(sessionEntry.deliveryContext),
origin: stripThreadIdFromOrigin(sessionEntry.origin),
};
}
if (!sessionEntry.chatType) {
sessionEntry.chatType = "direct";
}