mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 07:31:11 +00:00
fix: clear delivery routing state when creating isolated cron sessions (#27778)
* fix: clear delivery routing state when creating isolated cron sessions When `resolveCronSession()` creates a new session (forceNew / isolated), the `...entry` spread preserves `lastThreadId`, `lastTo`, `lastChannel`, and `lastAccountId` from the prior session. This causes announce-mode cron deliveries to post as thread replies instead of channel top-level messages when `delivery.to` matches the channel of a prior conversation. Clear delivery routing metadata on new session creation so isolated cron sessions start with a clean delivery state. Closes #27751 ✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved) * fix: also clear deliveryContext to prevent lastThreadId repopulation normalizeSessionEntryDelivery (called on store writes) repopulates lastThreadId from deliveryContext.threadId. Clearing only the last* fields is insufficient — deliveryContext must also be cleared when creating a new isolated session. ✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
This commit is contained in:
@@ -143,6 +143,94 @@ describe("resolveCronSession", () => {
|
||||
expect(result.sessionEntry.providerOverride).toBe("anthropic");
|
||||
});
|
||||
|
||||
it("clears delivery routing metadata and deliveryContext when forceNew is true", () => {
|
||||
const result = resolveWithStoredEntry({
|
||||
entry: {
|
||||
sessionId: "existing-session-id-789",
|
||||
updatedAt: NOW_MS - 1000,
|
||||
systemSent: true,
|
||||
lastChannel: "slack" as never,
|
||||
lastTo: "channel:C0XXXXXXXXX",
|
||||
lastAccountId: "acct-123",
|
||||
lastThreadId: "1737500000.123456",
|
||||
deliveryContext: {
|
||||
channel: "slack",
|
||||
to: "channel:C0XXXXXXXXX",
|
||||
threadId: "1737500000.123456",
|
||||
},
|
||||
modelOverride: "gpt-5.2",
|
||||
},
|
||||
fresh: true,
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
expect(result.isNewSession).toBe(true);
|
||||
// Delivery routing state must be cleared to prevent thread leaking.
|
||||
// deliveryContext must also be cleared because normalizeSessionEntryDelivery
|
||||
// repopulates lastThreadId from deliveryContext.threadId on store writes.
|
||||
expect(result.sessionEntry.lastChannel).toBeUndefined();
|
||||
expect(result.sessionEntry.lastTo).toBeUndefined();
|
||||
expect(result.sessionEntry.lastAccountId).toBeUndefined();
|
||||
expect(result.sessionEntry.lastThreadId).toBeUndefined();
|
||||
expect(result.sessionEntry.deliveryContext).toBeUndefined();
|
||||
// Per-session overrides must be preserved
|
||||
expect(result.sessionEntry.modelOverride).toBe("gpt-5.2");
|
||||
});
|
||||
|
||||
it("clears delivery routing metadata when session is stale", () => {
|
||||
const result = resolveWithStoredEntry({
|
||||
entry: {
|
||||
sessionId: "old-session-id",
|
||||
updatedAt: NOW_MS - 86_400_000,
|
||||
lastChannel: "slack" as never,
|
||||
lastTo: "channel:C0XXXXXXXXX",
|
||||
lastThreadId: "1737500000.999999",
|
||||
deliveryContext: {
|
||||
channel: "slack",
|
||||
to: "channel:C0XXXXXXXXX",
|
||||
threadId: "1737500000.999999",
|
||||
},
|
||||
},
|
||||
fresh: false,
|
||||
});
|
||||
|
||||
expect(result.isNewSession).toBe(true);
|
||||
expect(result.sessionEntry.lastChannel).toBeUndefined();
|
||||
expect(result.sessionEntry.lastTo).toBeUndefined();
|
||||
expect(result.sessionEntry.lastAccountId).toBeUndefined();
|
||||
expect(result.sessionEntry.lastThreadId).toBeUndefined();
|
||||
expect(result.sessionEntry.deliveryContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves delivery routing metadata when reusing fresh session", () => {
|
||||
const result = resolveWithStoredEntry({
|
||||
entry: {
|
||||
sessionId: "existing-session-id-101",
|
||||
updatedAt: NOW_MS - 1000,
|
||||
systemSent: true,
|
||||
lastChannel: "slack" as never,
|
||||
lastTo: "channel:C0XXXXXXXXX",
|
||||
lastThreadId: "1737500000.123456",
|
||||
deliveryContext: {
|
||||
channel: "slack",
|
||||
to: "channel:C0XXXXXXXXX",
|
||||
threadId: "1737500000.123456",
|
||||
},
|
||||
},
|
||||
fresh: true,
|
||||
});
|
||||
|
||||
expect(result.isNewSession).toBe(false);
|
||||
expect(result.sessionEntry.lastChannel).toBe("slack");
|
||||
expect(result.sessionEntry.lastTo).toBe("channel:C0XXXXXXXXX");
|
||||
expect(result.sessionEntry.lastThreadId).toBe("1737500000.123456");
|
||||
expect(result.sessionEntry.deliveryContext).toEqual({
|
||||
channel: "slack",
|
||||
to: "channel:C0XXXXXXXXX",
|
||||
threadId: "1737500000.123456",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates new sessionId when entry exists but has no sessionId", () => {
|
||||
const result = resolveWithStoredEntry({
|
||||
entry: {
|
||||
|
||||
@@ -65,6 +65,19 @@ export function resolveCronSession(params: {
|
||||
sessionId,
|
||||
updatedAt: params.nowMs,
|
||||
systemSent,
|
||||
// When starting a fresh session (forceNew / isolated), clear delivery routing
|
||||
// state inherited from prior sessions. Without this, lastThreadId leaks into
|
||||
// the new session and causes announce-mode cron deliveries to post as thread
|
||||
// replies instead of channel top-level messages.
|
||||
// deliveryContext must also be cleared because normalizeSessionEntryDelivery
|
||||
// repopulates lastThreadId from deliveryContext.threadId on store writes.
|
||||
...(isNewSession && {
|
||||
lastChannel: undefined,
|
||||
lastTo: undefined,
|
||||
lastAccountId: undefined,
|
||||
lastThreadId: undefined,
|
||||
deliveryContext: undefined,
|
||||
}),
|
||||
};
|
||||
return { storePath, store, sessionEntry, systemSent, isNewSession };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user