Batch inline comment wake payloads

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 09:55:41 -05:00
parent e75960f284
commit 91e040a696
14 changed files with 1049 additions and 9 deletions

View file

@ -193,6 +193,152 @@ export function joinPromptSections(
.join(separator);
}
type PaperclipWakeIssue = {
id: string | null;
identifier: string | null;
title: string | null;
status: string | null;
priority: string | null;
};
type PaperclipWakeComment = {
id: string | null;
issueId: string | null;
body: string;
bodyTruncated: boolean;
createdAt: string | null;
authorType: string | null;
authorId: string | null;
};
type PaperclipWakePayload = {
reason: string | null;
issue: PaperclipWakeIssue | null;
commentIds: string[];
latestCommentId: string | null;
comments: PaperclipWakeComment[];
requestedCount: number;
includedCount: number;
missingCount: number;
truncated: boolean;
fallbackFetchNeeded: boolean;
};
function normalizePaperclipWakeIssue(value: unknown): PaperclipWakeIssue | null {
const issue = parseObject(value);
const id = asString(issue.id, "").trim() || null;
const identifier = asString(issue.identifier, "").trim() || null;
const title = asString(issue.title, "").trim() || null;
const status = asString(issue.status, "").trim() || null;
const priority = asString(issue.priority, "").trim() || null;
if (!id && !identifier && !title) return null;
return {
id,
identifier,
title,
status,
priority,
};
}
function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | null {
const comment = parseObject(value);
const author = parseObject(comment.author);
const body = asString(comment.body, "");
if (!body.trim()) return null;
return {
id: asString(comment.id, "").trim() || null,
issueId: asString(comment.issueId, "").trim() || null,
body,
bodyTruncated: asBoolean(comment.bodyTruncated, false),
createdAt: asString(comment.createdAt, "").trim() || null,
authorType: asString(author.type, "").trim() || null,
authorId: asString(author.id, "").trim() || null,
};
}
export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayload | null {
const payload = parseObject(value);
const comments = Array.isArray(payload.comments)
? payload.comments
.map((entry) => normalizePaperclipWakeComment(entry))
.filter((entry): entry is PaperclipWakeComment => Boolean(entry))
: [];
const commentWindow = parseObject(payload.commentWindow);
const commentIds = Array.isArray(payload.commentIds)
? payload.commentIds
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
.map((entry) => entry.trim())
: [];
if (comments.length === 0 && commentIds.length === 0) return null;
return {
reason: asString(payload.reason, "").trim() || null,
issue: normalizePaperclipWakeIssue(payload.issue),
commentIds,
latestCommentId: asString(payload.latestCommentId, "").trim() || null,
comments,
requestedCount: asNumber(commentWindow.requestedCount, comments.length || commentIds.length),
includedCount: asNumber(commentWindow.includedCount, comments.length),
missingCount: asNumber(commentWindow.missingCount, 0),
truncated: asBoolean(payload.truncated, false),
fallbackFetchNeeded: asBoolean(payload.fallbackFetchNeeded, false),
};
}
export function stringifyPaperclipWakePayload(value: unknown): string | null {
const normalized = normalizePaperclipWakePayload(value);
if (!normalized) return null;
return JSON.stringify(normalized);
}
export function renderPaperclipWakePrompt(value: unknown): string {
const normalized = normalizePaperclipWakePayload(value);
if (!normalized) return "";
const lines = [
"## Paperclip Wake Payload",
"",
"Use this inline wake data first before refetching the issue thread.",
"Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch.",
"",
`- reason: ${normalized.reason ?? "unknown"}`,
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
];
if (normalized.issue?.status) {
lines.push(`- issue status: ${normalized.issue.status}`);
}
if (normalized.issue?.priority) {
lines.push(`- issue priority: ${normalized.issue.priority}`);
}
if (normalized.missingCount > 0) {
lines.push(`- omitted comments: ${normalized.missingCount}`);
}
lines.push("", "New comments in order:");
for (const [index, comment] of normalized.comments.entries()) {
const authorLabel = comment.authorId
? `${comment.authorType ?? "unknown"} ${comment.authorId}`
: comment.authorType ?? "unknown";
lines.push(
`${index + 1}. comment ${comment.id ?? "unknown"} at ${comment.createdAt ?? "unknown"} by ${authorLabel}`,
comment.body,
);
if (comment.bodyTruncated) {
lines.push("[comment body truncated]");
}
lines.push("");
}
return lines.join("\n").trim();
}
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
const redacted: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {

View file

@ -20,6 +20,8 @@ import {
ensurePathInEnv,
resolveCommandForLogs,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import {
@ -170,6 +172,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -189,6 +192,9 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -403,15 +409,18 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
const promptMetrics = {
promptChars: prompt.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -18,6 +18,8 @@ import {
resolveCommandForLogs,
resolvePaperclipDesiredSkillNames,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
joinPromptSections,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -313,6 +315,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
}
@ -331,6 +334,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -465,10 +471,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
@ -476,6 +484,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -19,6 +19,8 @@ import {
resolvePaperclipDesiredSkillNames,
removeMaintainerOnlySkillSymlinks,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
joinPromptSections,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -219,6 +221,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
}
@ -237,6 +240,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -357,11 +363,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const paperclipEnvNote = renderPaperclipEnvNote(env);
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
paperclipEnvNote,
renderedPrompt,
@ -370,6 +378,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
runtimeNoteChars: paperclipEnvNote.length,
heartbeatPromptChars: renderedPrompt.length,

View file

@ -22,6 +22,8 @@ import {
removeMaintainerOnlySkillSymlinks,
parseObject,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
@ -193,12 +195,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -300,12 +304,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const paperclipEnvNote = renderPaperclipEnvNote(env);
const apiAccessNote = renderApiAccessNote(env);
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
paperclipEnvNote,
apiAccessNote,
@ -315,6 +321,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars: instructionsPrefix.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
heartbeatPromptChars: renderedPrompt.length,

View file

@ -3,7 +3,14 @@ import type {
AdapterExecutionResult,
AdapterRuntimeServiceReport,
} from "@paperclipai/adapter-utils";
import { asNumber, asString, buildPaperclipEnv, parseObject } from "@paperclipai/adapter-utils/server-utils";
import {
asNumber,
asString,
buildPaperclipEnv,
parseObject,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
} from "@paperclipai/adapter-utils/server-utils";
import crypto, { randomUUID } from "node:crypto";
import { WebSocket } from "ws";
@ -335,7 +342,11 @@ function buildPaperclipEnvForWake(ctx: AdapterExecutionContext, wakePayload: Wak
return paperclipEnv;
}
function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string>): string {
function buildWakeText(
payload: WakePayload,
paperclipEnv: Record<string, string>,
structuredWakePrompt: string,
): string {
const claimedApiKeyPath = "~/.openclaw/workspace/paperclip-claimed-api-key.json";
const orderedKeys = [
"PAPERCLIP_RUN_ID",
@ -404,6 +415,12 @@ function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string
"- POST /api/issues/{issueId}/comments",
"- PATCH /api/issues/{issueId}",
"- POST /api/companies/{companyId}/issues (when asked to create a new issue)",
...(structuredWakePrompt
? [
"",
structuredWakePrompt,
]
: []),
"",
"Complete the workflow in this run.",
];
@ -415,6 +432,17 @@ function appendWakeText(baseText: string, wakeText: string): string {
return trimmedBase.length > 0 ? `${trimmedBase}\n\n${wakeText}` : wakeText;
}
function joinWakePayloadSections(structuredWakePrompt: string, structuredWakeJson: string): string {
const sections = [
structuredWakePrompt.trim(),
"Structured wake payload JSON:",
"```json",
structuredWakeJson,
"```",
].filter((entry) => entry.trim().length > 0);
return sections.join("\n");
}
function buildStandardPaperclipPayload(
ctx: AdapterExecutionContext,
wakePayload: WakePayload,
@ -447,6 +475,10 @@ function buildStandardPaperclipPayload(
approvalStatus: wakePayload.approvalStatus,
apiUrl: paperclipEnv.PAPERCLIP_API_URL ?? null,
};
const structuredWake = parseObject(ctx.context.paperclipWake);
if (Object.keys(structuredWake).length > 0) {
standardPaperclip.wake = structuredWake;
}
if (workspace) {
standardPaperclip.workspace = workspace;
@ -1053,7 +1085,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const wakePayload = buildWakePayload(ctx);
const paperclipEnv = buildPaperclipEnvForWake(ctx, wakePayload);
const wakeText = buildWakeText(wakePayload, paperclipEnv);
const structuredWakePrompt = renderPaperclipWakePrompt(ctx.context.paperclipWake);
const structuredWakeJson = stringifyPaperclipWakePayload(ctx.context.paperclipWake);
const wakeText = buildWakeText(
wakePayload,
paperclipEnv,
structuredWakeJson
? joinWakePayloadSections(structuredWakePrompt, structuredWakeJson)
: structuredWakePrompt,
);
const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);
const configuredSessionKey = nonEmpty(ctx.config.sessionKey);
@ -1075,6 +1115,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
idempotencyKey: ctx.runId,
};
delete agentParams.text;
agentParams.paperclip = paperclipPayload;
const configuredAgentId = nonEmpty(ctx.config.agentId);
if (configuredAgentId && !nonEmpty(agentParams.agentId)) {

View file

@ -17,6 +17,8 @@ import {
ensurePathInEnv,
resolveCommandForLogs,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
readPaperclipRuntimeSkillEntries,
resolvePaperclipDesiredSkillNames,
@ -154,12 +156,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -276,10 +280,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
@ -287,6 +293,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars: instructionsPrefix.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -20,6 +20,8 @@ import {
resolvePaperclipDesiredSkillNames,
removeMaintainerOnlySkillSymlinks,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
@ -177,6 +179,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
@ -184,6 +187,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (workspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -303,9 +307,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const userPrompt = joinPromptSections([
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedHeartbeatPrompt,
]);
@ -313,6 +319,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
systemPromptChars: renderedSystemPromptExtension.length,
promptChars: userPrompt.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedHeartbeatPrompt.length,
};