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:
Mitsuyuki Osabe
2026-03-01 02:09:12 +09:00
committed by GitHub
parent daa418895e
commit e1df1c60b8
2 changed files with 101 additions and 0 deletions

View File

@@ -143,6 +143,94 @@ describe("resolveCronSession", () => {
expect(result.sessionEntry.providerOverride).toBe("anthropic"); 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", () => { it("creates new sessionId when entry exists but has no sessionId", () => {
const result = resolveWithStoredEntry({ const result = resolveWithStoredEntry({
entry: { entry: {

View File

@@ -65,6 +65,19 @@ export function resolveCronSession(params: {
sessionId, sessionId,
updatedAt: params.nowMs, updatedAt: params.nowMs,
systemSent, 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 }; return { storePath, store, sessionEntry, systemSent, isNewSession };
} }