Merge pull request #2065 from edimuj/fix/heartbeat-session-reuse

fix: preserve session continuity for timer/heartbeat wakes
This commit is contained in:
Dotta 2026-03-31 08:29:45 -05:00 committed by GitHub
commit 9f1bb350fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 61 additions and 2 deletions

View file

@ -607,6 +607,14 @@ function parseIssueAssigneeAdapterOverrides(
};
}
/**
* Synthetic task key for timer/heartbeat wakes that have no issue context.
* This allows timer wakes to participate in the `agentTaskSessions` system
* and benefit from robust session resume, instead of relying solely on the
* simpler `agentRuntimeState.sessionId` fallback.
*/
const HEARTBEAT_TASK_KEY = "__heartbeat__";
function deriveTaskKey(
contextSnapshot: Record<string, unknown> | null | undefined,
payload: Record<string, unknown> | null | undefined,
@ -622,6 +630,28 @@ function deriveTaskKey(
);
}
/**
* Extended task key derivation that falls back to a stable synthetic key
* for timer/heartbeat wakes. This ensures timer wakes can resume their
* previous session via `agentTaskSessions` instead of starting fresh.
*
* The synthetic key is only used when:
* - No explicit task/issue key exists in the context
* - The wake source is "timer" (scheduled heartbeat)
*/
export function deriveTaskKeyWithHeartbeatFallback(
contextSnapshot: Record<string, unknown> | null | undefined,
payload: Record<string, unknown> | null | undefined,
) {
const explicit = deriveTaskKey(contextSnapshot, payload);
if (explicit) return explicit;
const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource);
if (wakeSource === "timer") return HEARTBEAT_TASK_KEY;
return null;
}
export function shouldResetTaskSessionForWake(
contextSnapshot: Record<string, unknown> | null | undefined,
) {
@ -1595,7 +1625,7 @@ export function heartbeatService(db: Db) {
) {
const contextSnapshot = parseObject(run.contextSnapshot);
const issueId = readNonEmptyString(contextSnapshot.issueId);
const taskKey = deriveTaskKey(contextSnapshot, null);
const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null);
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
const retryContextSnapshot = {
...contextSnapshot,
@ -2050,7 +2080,7 @@ export function heartbeatService(db: Db) {
const runtime = await ensureRuntimeState(agent);
const context = parseObject(run.contextSnapshot);
const taskKey = deriveTaskKey(context, null);
const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null);
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
const issueId = readNonEmptyString(context.issueId);
const issueContext = issueId