## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Issue comment wake handoffs are part of the control-plane execution loop that decides when agents resume work after comments and issue updates. > - PR #7678 changed that wake handoff behavior in server issue routes, heartbeat context, and related tests. > - The change broke an important workflow after merge, so the safest immediate fix is to restore the pre-#7678 wake behavior. > - This pull request reverts the wake-handoff behavior from PR #7678 while keeping narrow review-requested safeguards that prevent known runtime/test regressions. > - The benefit is that Paperclip returns to the last known working wake behavior without reintroducing avoidable UUID skill lookup and annotation-resolution test gaps. ## Linked Issues or Issue Description Refs: #7678 Bug context: - What happened: PR #7678 was reported to have broken an important Paperclip workflow after it merged. - Expected behavior: Paperclip should preserve the prior issue comment wake handoff behavior until a corrected change is ready. - Steps to reproduce: Use the workflow affected by PR #7678's issue comment wake handoff changes. - Paperclip version/commit: `master` after merge commit `4da79a88c67e54084d40bd18cada5ee5c8be23da`. - Deployment mode: Paperclip control-plane server behavior. ## What Changed - Reverted merge commit `4da79a88c67e54084d40bd18cada5ee5c8be23da` from PR #7678 to restore pre-#7678 wake-handoff behavior. - Preserved the safe accepted-plan routing check so `parseObject(...)` is not used as a boolean. - Preserved UUID filtering for run-scoped skill mentions so legacy non-UUID skill IDs do not reach a Postgres UUID lookup. - Restored the annotation thread-resolution test guard that verifies resolving a thread does not wake the assignee. ## Verification - `pnpm run preflight:workspace-links && NODE_ENV=test PAPERCLIP_HOME=/tmp/... PAPERCLIP_INSTANCE_ID=pap10614-revert TMPDIR=/tmp/... pnpm exec vitest run --project @paperclipai/server --no-file-parallelism --maxWorkers=1 server/src/__tests__/document-annotation-routes.test.ts server/src/__tests__/heartbeat-project-env.test.ts server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts server/src/__tests__/heartbeat-context-summary.test.ts` - Result: 4 test files passed, 26 tests passed. - Earlier targeted revert verification also passed: 4 test files, 50 tests. ## Risks - This intentionally restores behavior from before PR #7678, so intended wake-handoff improvements from that PR are removed. - The PR is no longer a byte-for-byte revert because Greptile identified two narrow safeguards worth preserving. - Low migration risk: no schema or dependency changes are included. - Follow-up work may still be needed to reintroduce the desired wake handoff behavior without the regression. ## Model Used OpenAI Codex, GPT-5 coding agent in this Paperclip heartbeat, with shell/tool execution and repository write access. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
@@ -528,9 +528,8 @@ export function extractMentionedSkillIdsFromSources(
|
||||
for (const source of sources) {
|
||||
if (typeof source !== "string" || source.length === 0) continue;
|
||||
for (const skillId of extractSkillMentionIds(source)) {
|
||||
if (isUuidLike(skillId)) {
|
||||
mentionedIds.add(skillId);
|
||||
}
|
||||
if (!isUuidLike(skillId)) continue;
|
||||
mentionedIds.add(skillId);
|
||||
}
|
||||
}
|
||||
return [...mentionedIds];
|
||||
@@ -2291,20 +2290,6 @@ function normalizeInteractionContinuationWakeContext(
|
||||
clearInteractionContinuationWakeContext(contextSnapshot);
|
||||
}
|
||||
|
||||
function isAcceptedPlanContinuationWakeContext(
|
||||
contextSnapshot: Record<string, unknown>,
|
||||
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;
|
||||
@@ -2411,7 +2396,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
exposeLowTrustRaw?: boolean;
|
||||
}) {
|
||||
const executionStage = parseObject(input.contextSnapshot.executionStage);
|
||||
let commentIds = extractWakeCommentIds(input.contextSnapshot);
|
||||
const commentIds = extractWakeCommentIds(input.contextSnapshot);
|
||||
const annotationCommentId = readNonEmptyString(input.contextSnapshot.annotationCommentId);
|
||||
const issueId = readNonEmptyString(input.contextSnapshot.issueId);
|
||||
const continuationSummary = input.continuationSummary ?? null;
|
||||
@@ -2431,28 +2416,6 @@ export async function buildPaperclipWakePayload(input: {
|
||||
.where(and(eq(issues.id, issueId), eq(issues.companyId, input.companyId)))
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null);
|
||||
let acceptedPlanCommentWindowTruncated = false;
|
||||
const acceptedPlanContinuationWake = isAcceptedPlanContinuationWakeContext(
|
||||
input.contextSnapshot,
|
||||
issueSummary?.workMode,
|
||||
);
|
||||
if (commentIds.length === 0 && acceptedPlanContinuationWake && issueSummary?.id) {
|
||||
const recentPlanCommentRows = await input.db
|
||||
.select({ id: issueComments.id })
|
||||
.from(issueComments)
|
||||
.where(and(
|
||||
eq(issueComments.companyId, input.companyId),
|
||||
eq(issueComments.issueId, issueSummary.id),
|
||||
isNull(issueComments.deletedAt),
|
||||
))
|
||||
.orderBy(desc(issueComments.createdAt))
|
||||
.limit(MAX_INLINE_WAKE_COMMENTS + 1);
|
||||
acceptedPlanCommentWindowTruncated = recentPlanCommentRows.length > MAX_INLINE_WAKE_COMMENTS;
|
||||
commentIds = recentPlanCommentRows
|
||||
.slice(0, MAX_INLINE_WAKE_COMMENTS)
|
||||
.reverse()
|
||||
.map((comment) => comment.id);
|
||||
}
|
||||
if (commentIds.length === 0 && Object.keys(executionStage).length === 0 && !issueSummary) return null;
|
||||
|
||||
const commentRows =
|
||||
@@ -2487,7 +2450,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
const commentsById = new Map(commentRows.map((comment) => [comment.id, comment]));
|
||||
const comments: Array<Record<string, unknown>> = [];
|
||||
let remainingBodyChars = MAX_INLINE_WAKE_COMMENT_BODY_TOTAL_CHARS;
|
||||
let truncated = acceptedPlanCommentWindowTruncated;
|
||||
let truncated = false;
|
||||
let missingCommentCount = 0;
|
||||
const safeContinuationSummary =
|
||||
continuationSummary && !input.exposeLowTrustRaw
|
||||
@@ -2625,9 +2588,6 @@ export async function buildPaperclipWakePayload(input: {
|
||||
: null,
|
||||
interactionKind: readNonEmptyString(input.contextSnapshot.interactionKind),
|
||||
interactionStatus: readNonEmptyString(input.contextSnapshot.interactionStatus),
|
||||
commentContextSource: acceptedPlanContinuationWake && commentIds.length > 0
|
||||
? "accepted_plan_confirmation"
|
||||
: null,
|
||||
checkedOutByHarness: input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true,
|
||||
dependencyBlockedInteraction: input.contextSnapshot.dependencyBlockedInteraction === true,
|
||||
treeHoldInteraction: input.contextSnapshot.treeHoldInteraction === true,
|
||||
@@ -2702,11 +2662,6 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
kind?: string | null;
|
||||
status?: string | null;
|
||||
} | null;
|
||||
acceptedPlanComments?: Array<{
|
||||
id?: string | null;
|
||||
authorType?: string | null;
|
||||
body?: string | null;
|
||||
}> | null;
|
||||
acceptedPlanContinuation?: boolean;
|
||||
}) {
|
||||
const quoteTaskScalar = (value: string) => JSON.stringify(value);
|
||||
@@ -2767,28 +2722,6 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
if (wakeComment?.body.trim()) {
|
||||
lines.push("", "Latest wake comment:", fenceTaskText(wakeComment.body.trim()));
|
||||
}
|
||||
if (acceptedPlanContinuation) {
|
||||
const acceptedPlanComments = (input.acceptedPlanComments ?? [])
|
||||
.map((comment) => ({
|
||||
...comment,
|
||||
body: comment.body?.trim() ?? "",
|
||||
}))
|
||||
.filter((comment) => comment.body.length > 0)
|
||||
.slice(0, MAX_INLINE_WAKE_COMMENTS);
|
||||
if (acceptedPlanComments.length > 0) {
|
||||
lines.push("", "Comments included with the confirmed plan:");
|
||||
for (const [index, comment] of acceptedPlanComments.entries()) {
|
||||
const authorType = comment.authorType?.trim();
|
||||
const commentId = comment.id?.trim();
|
||||
const labelParts = [
|
||||
`Comment ${index + 1}`,
|
||||
...(authorType ? [authorType] : []),
|
||||
...(commentId ? [commentId] : []),
|
||||
];
|
||||
lines.push("", `${labelParts.join(" - ")}:`, fenceTaskText(comment.body));
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push("", "Use this task context as the current assignment.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -7815,7 +7748,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
companyId: agent.companyId,
|
||||
agentId: agent.id,
|
||||
issueId,
|
||||
acceptedPlanContinuationWake: isAcceptedPlanContinuationWakeContext(context, issueContext.workMode),
|
||||
acceptedPlanContinuationWake:
|
||||
readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation"
|
||||
|| (
|
||||
issueContext.workMode === "planning"
|
||||
&& readNonEmptyString(context.interactionKind) === "request_confirmation"
|
||||
&& readNonEmptyString(context.interactionStatus) === "accepted"
|
||||
),
|
||||
contextSnapshot: context,
|
||||
})
|
||||
: null;
|
||||
@@ -7954,18 +7893,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
} else {
|
||||
delete context[PAPERCLIP_WAKE_PAYLOAD_KEY];
|
||||
}
|
||||
const acceptedPlanWakeRouting = parseObject(context.acceptedPlanWakeRouting);
|
||||
const acceptedPlanContinuationForTask =
|
||||
isAcceptedPlanContinuationWakeContext(context, issueRef?.workMode) &&
|
||||
Object.keys(acceptedPlanWakeRouting).length === 0;
|
||||
const acceptedPlanCommentsForTask =
|
||||
acceptedPlanContinuationForTask && Array.isArray(paperclipWakePayload?.comments)
|
||||
? paperclipWakePayload.comments.map((comment) => ({
|
||||
id: readNonEmptyString(comment.id),
|
||||
authorType: readNonEmptyString(comment.authorType),
|
||||
body: readNonEmptyString(comment.body),
|
||||
}))
|
||||
: null;
|
||||
const taskMarkdown = buildPaperclipTaskMarkdown({
|
||||
issue: issueRef
|
||||
? {
|
||||
@@ -7981,8 +7908,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
kind: readNonEmptyString(context.interactionKind),
|
||||
status: readNonEmptyString(context.interactionStatus),
|
||||
},
|
||||
acceptedPlanComments: acceptedPlanCommentsForTask,
|
||||
acceptedPlanContinuation: acceptedPlanContinuationForTask,
|
||||
acceptedPlanContinuation:
|
||||
readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation"
|
||||
&& Object.keys(parseObject(context.acceptedPlanWakeRouting)).length === 0,
|
||||
});
|
||||
if (issueRef) {
|
||||
context.paperclipIssue = {
|
||||
|
||||
Reference in New Issue
Block a user