From a379a0264d384510ff8ac4a47fb1e44d7b556f68 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 7 Jun 2026 00:54:53 +0000 Subject: [PATCH] Fix accepted plan wake comment handoff Co-Authored-By: Paperclip --- ...at-accepted-plan-workspace-refresh.test.ts | 20 ++++++++++ server/src/services/heartbeat.ts | 40 +++++++++++-------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts b/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts index 11f917854ff..831a6c95881 100644 --- a/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts +++ b/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts @@ -737,6 +737,17 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => { createdAt: new Date(), updatedAt: new Date(), }); + const planCommentId = randomUUID(); + await db.insert(issueComments).values({ + id: planCommentId, + companyId, + issueId, + authorUserId: "board-user-1", + authorType: "user", + body: "Keep the accepted plan rollout split into separate child tasks.", + createdAt: new Date("2026-06-07T00:00:00.000Z"), + updatedAt: new Date("2026-06-07T00:00:00.000Z"), + }); await seedAcceptedPlanClaim({ companyId, issueId, @@ -803,5 +814,14 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => { expect(adapterInput.runtime.sessionId).toBe("accepted-plan-retry-session"); expect(adapterInput.context.acceptedPlanWakeRouting).toBeUndefined(); expect(adapterInput.context.paperclipTaskMarkdown).toContain("Create child issues from the approved plan only"); + expect(adapterInput.context.paperclipTaskMarkdown).toContain("Comments included with the confirmed plan:"); + expect(adapterInput.context.paperclipTaskMarkdown).toContain(planCommentId); + expect(adapterInput.context.paperclipTaskMarkdown).toContain( + "Keep the accepted plan rollout split into separate child tasks.", + ); + expect(adapterInput.context.paperclipWake).toEqual(expect.objectContaining({ + commentIds: [planCommentId], + commentContextSource: "accepted_plan_confirmation", + })); }, 20_000); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 4aadd872c59..b54cb59c405 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2291,6 +2291,20 @@ function normalizeInteractionContinuationWakeContext( clearInteractionContinuationWakeContext(contextSnapshot); } +function isAcceptedPlanContinuationWakeContext( + contextSnapshot: Record, + issueWorkMode?: string | null, +) { + return ( + readNonEmptyString(contextSnapshot.workspaceRefreshReason) === "accepted_plan_confirmation" || + ( + issueWorkMode === "planning" && + readNonEmptyString(contextSnapshot.interactionKind) === "request_confirmation" && + readNonEmptyString(contextSnapshot.interactionStatus) === "accepted" + ) + ); +} + type AcceptedPlanWakeRoutingDecision = { otherActiveClaimIssueId: string; otherActiveClaimIdentifier: string | null; @@ -2418,13 +2432,10 @@ export async function buildPaperclipWakePayload(input: { .then((rows) => rows[0] ?? null) : null); let acceptedPlanCommentWindowTruncated = false; - const acceptedPlanContinuationWake = - readNonEmptyString(input.contextSnapshot.workspaceRefreshReason) === "accepted_plan_confirmation" || - ( - issueSummary?.workMode === "planning" && - readNonEmptyString(input.contextSnapshot.interactionKind) === "request_confirmation" && - readNonEmptyString(input.contextSnapshot.interactionStatus) === "accepted" - ); + const acceptedPlanContinuationWake = isAcceptedPlanContinuationWakeContext( + input.contextSnapshot, + issueSummary?.workMode, + ); if (commentIds.length === 0 && acceptedPlanContinuationWake && issueSummary?.id) { const recentPlanCommentRows = await input.db .select({ id: issueComments.id }) @@ -2434,7 +2445,7 @@ export async function buildPaperclipWakePayload(input: { eq(issueComments.issueId, issueSummary.id), isNull(issueComments.deletedAt), )) - .orderBy(desc(issueComments.createdAt), desc(issueComments.id)) + .orderBy(desc(issueComments.createdAt)) .limit(MAX_INLINE_WAKE_COMMENTS + 1); acceptedPlanCommentWindowTruncated = recentPlanCommentRows.length > MAX_INLINE_WAKE_COMMENTS; commentIds = recentPlanCommentRows @@ -7804,13 +7815,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) companyId: agent.companyId, agentId: agent.id, issueId, - acceptedPlanContinuationWake: - readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation" - || ( - issueContext.workMode === "planning" - && readNonEmptyString(context.interactionKind) === "request_confirmation" - && readNonEmptyString(context.interactionStatus) === "accepted" - ), + acceptedPlanContinuationWake: isAcceptedPlanContinuationWakeContext(context, issueContext.workMode), contextSnapshot: context, }) : null; @@ -7949,9 +7954,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } else { delete context[PAPERCLIP_WAKE_PAYLOAD_KEY]; } + const acceptedPlanWakeRouting = parseObject(context.acceptedPlanWakeRouting); const acceptedPlanContinuationForTask = - readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation" - && !parseObject(context.acceptedPlanWakeRouting); + isAcceptedPlanContinuationWakeContext(context, issueRef?.workMode) && + Object.keys(acceptedPlanWakeRouting).length === 0; const acceptedPlanCommentsForTask = acceptedPlanContinuationForTask && Array.isArray(paperclipWakePayload?.comments) ? paperclipWakePayload.comments.map((comment) => ({