From e31d77bc47fbbca315de679bf87a29c66faa3895 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 5 Mar 2026 09:48:11 -0600 Subject: [PATCH] Reset sessions for manual and timer heartbeat wakes --- .../heartbeat-workspace-session.test.ts | 22 +++++++++++++ server/src/services/heartbeat.ts | 33 ++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index 9a34402a..650556d1 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -93,6 +93,19 @@ describe("shouldResetTaskSessionForWake", () => { expect(shouldResetTaskSessionForWake({ wakeReason: "issue_assigned" })).toBe(true); }); + it("resets session context on timer heartbeats", () => { + expect(shouldResetTaskSessionForWake({ wakeSource: "timer" })).toBe(true); + }); + + it("resets session context on manual on-demand invokes", () => { + expect( + shouldResetTaskSessionForWake({ + wakeSource: "on_demand", + wakeTriggerDetail: "manual", + }), + ).toBe(true); + }); + it("does not reset session context on mention wake comment", () => { expect( shouldResetTaskSessionForWake({ @@ -118,4 +131,13 @@ describe("shouldResetTaskSessionForWake", () => { it("does not reset when wake reason is missing", () => { expect(shouldResetTaskSessionForWake({})).toBe(false); }); + + it("does not reset session context on callback on-demand invokes", () => { + expect( + shouldResetTaskSessionForWake({ + wakeSource: "on_demand", + wakeTriggerDetail: "callback", + }), + ).toBe(false); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 88155bbc..2749a198 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -199,7 +199,29 @@ export function shouldResetTaskSessionForWake( contextSnapshot: Record | null | undefined, ) { const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); - return wakeReason === "issue_assigned"; + if (wakeReason === "issue_assigned") return true; + + const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource); + if (wakeSource === "timer") return true; + + const wakeTriggerDetail = readNonEmptyString(contextSnapshot?.wakeTriggerDetail); + return wakeSource === "on_demand" && wakeTriggerDetail === "manual"; +} + +function describeSessionResetReason( + contextSnapshot: Record | null | undefined, +) { + const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); + if (wakeReason === "issue_assigned") return "wake reason is issue_assigned"; + + const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource); + if (wakeSource === "timer") return "wake source is timer"; + + const wakeTriggerDetail = readNonEmptyString(contextSnapshot?.wakeTriggerDetail); + if (wakeSource === "on_demand" && wakeTriggerDetail === "manual") { + return "this is a manual invoke"; + } + return null; } function deriveCommentId( @@ -1066,6 +1088,7 @@ export function heartbeatService(db: Db) { ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey) : null; const resetTaskSession = shouldResetTaskSessionForWake(context); + const sessionResetReason = describeSessionResetReason(context); const taskSessionForRun = resetTaskSession ? null : taskSession; const previousSessionParams = normalizeSessionParams( sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null), @@ -1085,9 +1108,11 @@ export function heartbeatService(db: Db) { const runtimeWorkspaceWarnings = [ ...resolvedWorkspace.warnings, ...(runtimeSessionResolution.warning ? [runtimeSessionResolution.warning] : []), - ...(resetTaskSession && taskKey + ...(resetTaskSession && sessionResetReason ? [ - `Skipping saved session resume for task "${taskKey}" because wake reason is issue_assigned.`, + taskKey + ? `Skipping saved session resume for task "${taskKey}" because ${sessionResetReason}.` + : `Skipping saved session resume because ${sessionResetReason}.`, ] : []), ]; @@ -1103,7 +1128,7 @@ export function heartbeatService(db: Db) { if (resolvedWorkspace.projectId && !readNonEmptyString(context.projectId)) { context.projectId = resolvedWorkspace.projectId; } - const runtimeSessionFallback = taskKey ? null : runtime.sessionId; + const runtimeSessionFallback = taskKey || resetTaskSession ? null : runtime.sessionId; const previousSessionDisplayId = truncateDisplayId( taskSessionForRun?.sessionDisplayId ?? (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(runtimeSessionParams) : null) ??