Skip to content

Commit e1df1c6

Browse files
author
Mitsuyuki Osabe
authored
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)
1 parent daa4188 commit e1df1c6

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

src/cron/isolated-agent/session.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,94 @@ describe("resolveCronSession", () => {
143143
expect(result.sessionEntry.providerOverride).toBe("anthropic");
144144
});
145145

146+
it("clears delivery routing metadata and deliveryContext when forceNew is true", () => {
147+
const result = resolveWithStoredEntry({
148+
entry: {
149+
sessionId: "existing-session-id-789",
150+
updatedAt: NOW_MS - 1000,
151+
systemSent: true,
152+
lastChannel: "slack" as never,
153+
lastTo: "channel:C0XXXXXXXXX",
154+
lastAccountId: "acct-123",
155+
lastThreadId: "1737500000.123456",
156+
deliveryContext: {
157+
channel: "slack",
158+
to: "channel:C0XXXXXXXXX",
159+
threadId: "1737500000.123456",
160+
},
161+
modelOverride: "gpt-5.2",
162+
},
163+
fresh: true,
164+
forceNew: true,
165+
});
166+
167+
expect(result.isNewSession).toBe(true);
168+
// Delivery routing state must be cleared to prevent thread leaking.
169+
// deliveryContext must also be cleared because normalizeSessionEntryDelivery
170+
// repopulates lastThreadId from deliveryContext.threadId on store writes.
171+
expect(result.sessionEntry.lastChannel).toBeUndefined();
172+
expect(result.sessionEntry.lastTo).toBeUndefined();
173+
expect(result.sessionEntry.lastAccountId).toBeUndefined();
174+
expect(result.sessionEntry.lastThreadId).toBeUndefined();
175+
expect(result.sessionEntry.deliveryContext).toBeUndefined();
176+
// Per-session overrides must be preserved
177+
expect(result.sessionEntry.modelOverride).toBe("gpt-5.2");
178+
});
179+
180+
it("clears delivery routing metadata when session is stale", () => {
181+
const result = resolveWithStoredEntry({
182+
entry: {
183+
sessionId: "old-session-id",
184+
updatedAt: NOW_MS - 86_400_000,
185+
lastChannel: "slack" as never,
186+
lastTo: "channel:C0XXXXXXXXX",
187+
lastThreadId: "1737500000.999999",
188+
deliveryContext: {
189+
channel: "slack",
190+
to: "channel:C0XXXXXXXXX",
191+
threadId: "1737500000.999999",
192+
},
193+
},
194+
fresh: false,
195+
});
196+
197+
expect(result.isNewSession).toBe(true);
198+
expect(result.sessionEntry.lastChannel).toBeUndefined();
199+
expect(result.sessionEntry.lastTo).toBeUndefined();
200+
expect(result.sessionEntry.lastAccountId).toBeUndefined();
201+
expect(result.sessionEntry.lastThreadId).toBeUndefined();
202+
expect(result.sessionEntry.deliveryContext).toBeUndefined();
203+
});
204+
205+
it("preserves delivery routing metadata when reusing fresh session", () => {
206+
const result = resolveWithStoredEntry({
207+
entry: {
208+
sessionId: "existing-session-id-101",
209+
updatedAt: NOW_MS - 1000,
210+
systemSent: true,
211+
lastChannel: "slack" as never,
212+
lastTo: "channel:C0XXXXXXXXX",
213+
lastThreadId: "1737500000.123456",
214+
deliveryContext: {
215+
channel: "slack",
216+
to: "channel:C0XXXXXXXXX",
217+
threadId: "1737500000.123456",
218+
},
219+
},
220+
fresh: true,
221+
});
222+
223+
expect(result.isNewSession).toBe(false);
224+
expect(result.sessionEntry.lastChannel).toBe("slack");
225+
expect(result.sessionEntry.lastTo).toBe("channel:C0XXXXXXXXX");
226+
expect(result.sessionEntry.lastThreadId).toBe("1737500000.123456");
227+
expect(result.sessionEntry.deliveryContext).toEqual({
228+
channel: "slack",
229+
to: "channel:C0XXXXXXXXX",
230+
threadId: "1737500000.123456",
231+
});
232+
});
233+
146234
it("creates new sessionId when entry exists but has no sessionId", () => {
147235
const result = resolveWithStoredEntry({
148236
entry: {

src/cron/isolated-agent/session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ export function resolveCronSession(params: {
6565
sessionId,
6666
updatedAt: params.nowMs,
6767
systemSent,
68+
// When starting a fresh session (forceNew / isolated), clear delivery routing
69+
// state inherited from prior sessions. Without this, lastThreadId leaks into
70+
// the new session and causes announce-mode cron deliveries to post as thread
71+
// replies instead of channel top-level messages.
72+
// deliveryContext must also be cleared because normalizeSessionEntryDelivery
73+
// repopulates lastThreadId from deliveryContext.threadId on store writes.
74+
...(isNewSession && {
75+
lastChannel: undefined,
76+
lastTo: undefined,
77+
lastAccountId: undefined,
78+
lastThreadId: undefined,
79+
deliveryContext: undefined,
80+
}),
6881
};
6982
return { storePath, store, sessionEntry, systemSent, isNewSession };
7083
}

0 commit comments

Comments
 (0)