fix: preserve session continuity for timer/heartbeat wakes

Timer wakes had no taskKey, so they couldn't use agentTaskSessions for
session resume. Adds a synthetic __heartbeat__ task key for timer wakes
so they participate in the full session system.

Includes 6 dedicated unit tests for deriveTaskKeyWithHeartbeatFallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edin Mujkanovic 2026-03-29 18:14:03 +02:00
parent 6a72faf83b
commit 70702ce74f
2 changed files with 61 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import { sessionCodec as codexSessionCodec } from "@paperclipai/adapter-codex-lo
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
import {
buildExplicitResumeSessionOverride,
deriveTaskKeyWithHeartbeatFallback,
formatRuntimeWorkspaceWarningLog,
prioritizeProjectWorkspaceCandidatesForRun,
parseSessionCompactionPolicy,
@ -184,6 +185,34 @@ describe("shouldResetTaskSessionForWake", () => {
});
});
describe("deriveTaskKeyWithHeartbeatFallback", () => {
it("returns explicit taskKey when present", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ taskKey: "issue-123" }, null)).toBe("issue-123");
});
it("returns explicit issueId when no taskKey", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ issueId: "issue-456" }, null)).toBe("issue-456");
});
it("returns __heartbeat__ for timer wakes with no explicit key", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ wakeSource: "timer" }, null)).toBe("__heartbeat__");
});
it("prefers explicit key over heartbeat fallback even on timer wakes", () => {
expect(
deriveTaskKeyWithHeartbeatFallback({ wakeSource: "timer", taskKey: "issue-789" }, null),
).toBe("issue-789");
});
it("returns null for non-timer wakes with no explicit key", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ wakeSource: "on_demand" }, null)).toBeNull();
});
it("returns null for empty context", () => {
expect(deriveTaskKeyWithHeartbeatFallback({}, null)).toBeNull();
});
});
describe("buildExplicitResumeSessionOverride", () => {
it("reuses saved task session params when they belong to the selected failed run", () => {
const result = buildExplicitResumeSessionOverride({