refactor: remove the wake payload from the launch environment

The server sent the wake payload two times: once as fenced JSON in the
rendered prompt, and once as the PAPERCLIP_WAKE_PAYLOAD_JSON launch
environment variable. Only the environment copy passed through the
base64 launch envelope, and only that envelope has a per-string size
limit (131,072 bytes). A normal driver task's payload can use over
100% of that limit on its own.

The prompt is the single delivery path now. No production code read
the environment variable; only the agent skill told the agent to
check it. Every writer of the variable also rendered the prompt from
the same normalized payload, so no content is lost.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
nickyleach 2026-09-10 08:16:35 +00:00 committed by Nicky Leach
parent 9effe51b63
commit fe4f55462c
No known key found for this signature in database
GPG Key ID: 4861541D36B2037E
17 changed files with 441 additions and 64 deletions

View File

@ -579,7 +579,9 @@ describe("shared ACPX engine runtime behavior", () => {
expect(prompt).toContain("Paperclip runtime note:");
expect(prompt).toContain("PAPERCLIP_AGENT_ID");
expect(prompt).toContain("PAPERCLIP_API_KEY");
expect(prompt).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
// The wake payload rides only the prompt body now, not a launch env var,
// so it no longer appears in the PAPERCLIP_* runtime note list.
expect(prompt).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(prompt).toContain("Paperclip API access note:");
expect(prompt).toContain('PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"; PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"');
expect(prompt).toContain("$PAPERCLIP_API_BASE/api/agents/me");

View File

@ -70,7 +70,6 @@ import {
removeMaintainerOnlySkillSymlinks,
rewriteWorkspaceCwdEnvVarsForExecution,
shapePaperclipWorkspaceEnvForExecution,
stringifyPaperclipWakePayload,
type PaperclipSkillEntry,
} from "@paperclipai/adapter-utils/server-utils";
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
@ -1869,7 +1868,6 @@ async function buildRuntime(input: {
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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
@ -1878,7 +1876,6 @@ async function buildRuntime(input: {
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;
applyPaperclipWorkspaceEnv(env, {
workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
workspaceSource,

View File

@ -455,6 +455,70 @@ describe("sandbox adapter execution targets", () => {
}
});
it("keeps the launch envelope under the kernel per-string limit even with a large env value", async () => {
const rootDir = await mkdtemp(
path.join(os.tmpdir(), "paperclip-process-session-envelope-"),
);
cleanupDirs.push(rootDir);
const childPath = path.join(rootDir, "noop-acp-child.mjs");
await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8");
const delegate = createLocalSandboxRunner();
const execScripts: string[] = [];
const runner = {
execute: vi.fn(async (input: Parameters<typeof delegate.execute>[0]) => {
execScripts.push(input.args?.[1] ?? "");
return delegate.execute(input);
}),
};
const target: AdapterSandboxExecutionTarget = {
kind: "remote",
transport: "sandbox",
providerKey: "local-test",
remoteCwd: rootDir,
timeoutMs: 30_000,
runner,
};
// A 90,000-byte single env value stands in for a wake payload sized
// field. The old PAPERCLIP_WAKE_PAYLOAD_JSON writer put a value close to
// this size into the launch env on a normal driver task.
const largeEnvValue = "x".repeat(90_000);
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
runId: "run-process-session-envelope",
target,
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
adapterKey: "acpx",
command: process.execPath,
args: [childPath],
cwd: rootDir,
env: { PAPERCLIP_LARGE_FIELD: largeEnvValue },
timeoutSec: 5,
onLog: async () => {},
});
expect(bridge).not.toBeNull();
try {
const launchExec = execScripts.find((script) =>
script.includes("PAPERCLIP_PROCESS_SESSION_COMMAND_B64="),
);
expect(launchExec).toBeDefined();
// shellQuote wraps the base64 payload in plain single quotes -- the
// base64 alphabet has no single quote, so no escaping is present.
const match = launchExec!.match(
/PAPERCLIP_PROCESS_SESSION_COMMAND_B64='([^']*)'/,
);
expect(match).not.toBeNull();
const commandPayloadBase64 = match![1];
// A 32-page (4096-byte page) kernel MAX_ARG_STRLEN of 131,072 bytes
// bounds a single argv/environ string, including this base64 payload.
expect(commandPayloadBase64.length).toBeLessThan(131_072);
} finally {
await bridge?.stop();
}
});
it.each([
{ outputMode: "polled", streamOutputViaSession: false },
{ outputMode: "streamed", streamOutputViaSession: true },

View File

@ -2927,6 +2927,274 @@ describe("renderPaperclipWakePrompt", () => {
expect(prompt).toContain("PAP-101 Implement helper (done)");
expect(prompt).toContain("Added the helper route and tests.");
});
// Locks the property the env-var removal (PAP-6176) depends on: the prompt
// is the single delivery path, so every field the payload carries must
// reach the prompt text. A handful of fields never render as prompt text
// under any one scenario — `commentIds`/`interactionKind`/`interactionStatus`
// only surface together on a planning wake with zero comments (mutually
// exclusive with the comment-list scenario below), `unresolvedBlockerIssueIds`
// is shadowed by `unresolvedBlockerSummaries` when both are set, and the
// top-level `truncated` field is not read by the renderer at all. Those five
// are still checked through the JSON round trip, proving the env-var copy
// and the prompt copy come from the same normalized object.
it("the prompt carries every top-level wake payload field", () => {
const payload = {
reason: "issue_commented",
issue: {
id: "wakecov-issue-id",
identifier: "PAP-9001",
title: "wakecov-issue-title",
description: "wakecov-issue-description",
descriptionTruncated: false,
status: "wakecov-issue-status",
workMode: "wakecov-issue-workmode",
priority: "wakecov-issue-priority",
},
executionContinuation: {
version: 1,
companyId: "wakecov-company-id",
issueId: "wakecov-issue-id",
trigger: { reason: "issue_commented", interactionId: null, sourceRunId: null },
originCommentIds: [],
objective: "wakecov-objective",
messages: [
{
id: "wakecov-message-id",
authorType: "user",
authorId: "wakecov-message-author",
body: "wakecov-message-body",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
deleted: false,
sourceTrust: "trusted",
},
],
interactionOutcomes: [
{ id: "wakecov-outcome-id", kind: "wakecov-outcome-kind", status: "resolved", result: { note: "wakecov-interaction-result" } },
],
recoveryOutcomes: [{ recoveryActionId: "wakecov-recovery-action-id", decision: "retry" }],
completedWork: "wakecov-completed-work",
completedActions: [{ runId: "wakecov-run-id", receiptId: "wakecov-receipt-id", operationId: "wakecov-op-id", result: { ok: true } }],
unresolvedInteractionIds: [],
coverage: { kind: "full_task_history", throughCommentId: null, summaryThroughCommentId: null },
},
recovery: {
cause: "process_lost",
failureSummary: "wakecov-recovery-failure",
originalAssignee: { id: "wakecov-orig-assignee-id", name: null },
attemptCount: 2,
maxAttempts: 5,
nextAction: "wakecov-recovery-next-action",
routingFallbackReason: "wakecov-routing-fallback-reason",
},
checkedOutByHarness: true,
simplifiedEnglishInteractions: true,
dependencyBlockedInteraction: true,
treeHoldInteraction: true,
activeTreeHold: { holdId: "wakecov-hold-id", rootIssueId: "wakecov-hold-root", mode: "wakecov-hold-mode" },
unresolvedBlockerIssueIds: ["wakecov-blocker-issue-id"],
unresolvedBlockerSummaries: [
{ id: "wakecov-blocker-id", identifier: "wakecov-blocker-identifier", title: "wakecov-blocker-title", status: "wakecov-blocker-status" },
],
executionStage: {
wakeRole: "reviewer",
stageId: "wakecov-stage-id",
stageType: "wakecov-stage-type",
currentParticipant: { type: "agent", agentId: "wakecov-participant-agent" },
returnAssignee: { type: "user", userId: "wakecov-return-user" },
reviewRequest: { instructions: "wakecov-review-instructions" },
lastDecisionOutcome: "wakecov-decision-outcome",
allowedActions: ["wakecov-allowed-action"],
},
continuationSummary: { body: "wakecov-continuation-summary-body" },
planReviewContext: null,
documentReviewContext: null,
annotationDeltas: [],
livenessContinuation: {
attempt: 3,
maxAttempts: 6,
sourceRunId: "wakecov-liveness-run-id",
state: "wakecov-liveness-state",
reason: "wakecov-liveness-reason",
instruction: "wakecov-liveness-instruction",
},
taskWatchdog: null,
interactionKind: "wakecov-interaction-kind-field",
interactionStatus: "wakecov-interaction-status-field",
checkboxSelection: {
prompt: "wakecov-checkbox-prompt",
selectedOptionIds: ["wakecov-option-id"],
selectedOptions: [{ id: "wakecov-option-id", label: "wakecov-option-label", description: "wakecov-option-description" }],
},
questionResponse: {
interactionId: "wakecov-question-interaction-id",
summaryMarkdown: "wakecov-question-summary",
},
executionWorkspace: { branchName: "wakecov-branch-name" },
agentMessage: { text: "wakecov-agent-message-text", source: "plugin", pluginKey: "wakecov-plugin-key" },
childIssueSummaries: [
{ id: "wakecov-child-id", identifier: "wakecov-child-identifier", title: "wakecov-child-title", status: "wakecov-child-status", summary: "wakecov-child-summary" },
],
childIssueSummaryTruncated: true,
commentIds: ["wakecov-comment-id-a"],
latestCommentId: "wakecov-latest-comment-id",
comments: [
{
id: "wakecov-comment-id",
body: "wakecov-comment-body",
createdAt: "2026-01-01T00:00:00.000Z",
author: { type: "user", id: "wakecov-comment-author-id" },
},
],
commentWindow: { requestedCount: 3, includedCount: 1, missingCount: 2 },
truncated: true,
fallbackFetchNeeded: true,
};
const serialized = stringifyPaperclipWakePayload(payload);
expect(serialized).not.toBeNull();
const parsed = JSON.parse(serialized ?? "{}");
// Fields with no distinct prompt rendering under this scenario (see the
// comment above the test): confirm the env-var copy still carried them.
expect(parsed).toMatchObject({
commentIds: ["wakecov-comment-id-a"],
interactionKind: "wakecov-interaction-kind-field",
interactionStatus: "wakecov-interaction-status-field",
unresolvedBlockerIssueIds: ["wakecov-blocker-issue-id"],
truncated: true,
});
const prompt = renderPaperclipWakePrompt(payload);
// reason
expect(prompt).toContain("- reason: issue_commented");
// issue
expect(prompt).toContain("- issue: PAP-9001 wakecov-issue-title");
expect(prompt).toContain("wakecov-issue-description");
expect(prompt).toContain("- issue status: wakecov-issue-status");
expect(prompt).toContain("- issue work mode: wakecov-issue-workmode");
expect(prompt).toContain("- issue priority: wakecov-issue-priority");
// executionContinuation (objective, messages, completedWork, interactionOutcomes, completedActions, recoveryOutcomes)
expect(prompt).toContain("wakecov-objective");
expect(prompt).toContain("wakecov-message-body");
expect(prompt).toContain("wakecov-completed-work");
expect(prompt).toContain("wakecov-interaction-result");
expect(prompt).toContain("wakecov-receipt-id");
expect(prompt).toContain("wakecov-recovery-action-id");
// recovery
expect(prompt).toContain("- failure summary: wakecov-recovery-failure");
expect(prompt).toContain("- original assignee: wakecov-orig-assignee-id");
expect(prompt).toContain("- recovery attempt: 2/5");
expect(prompt).toContain("- next action: wakecov-recovery-next-action");
expect(prompt).toContain("- routing fallback: wakecov-routing-fallback-reason");
// checkedOutByHarness
expect(prompt).toContain("The harness already checked out this issue for the current run.");
// simplifiedEnglishInteractions
expect(prompt).toContain("ASD-STE100 Simplified Technical English");
// dependencyBlockedInteraction + unresolvedBlockerSummaries
expect(prompt).toContain("- dependency-blocked interaction: yes");
expect(prompt).toContain("wakecov-blocker-identifier wakecov-blocker-title (wakecov-blocker-status)");
// treeHoldInteraction + activeTreeHold
expect(prompt).toContain("- tree-hold interaction: yes");
expect(prompt).toContain("wakecov-hold-id rooted at wakecov-hold-root (wakecov-hold-mode)");
// executionStage
expect(prompt).toContain("- execution wake role: reviewer");
expect(prompt).toContain("- execution stage: wakecov-stage-type");
expect(prompt).toContain("agent wakecov-participant-agent");
expect(prompt).toContain("user wakecov-return-user");
expect(prompt).toContain("- last decision outcome: wakecov-decision-outcome");
expect(prompt).toContain("wakecov-allowed-action");
expect(prompt).toContain("wakecov-review-instructions");
// continuationSummary
expect(prompt).toContain("wakecov-continuation-summary-body");
// livenessContinuation
expect(prompt).toContain("- attempt: 3/6");
expect(prompt).toContain("- source run: wakecov-liveness-run-id");
expect(prompt).toContain("- liveness state: wakecov-liveness-state");
expect(prompt).toContain("- reason: wakecov-liveness-reason");
expect(prompt).toContain("- instruction: wakecov-liveness-instruction");
// checkboxSelection
expect(prompt).toContain("- checkbox prompt: wakecov-checkbox-prompt");
expect(prompt).toContain("wakecov-option-id");
expect(prompt).toContain("wakecov-option-label");
expect(prompt).toContain("wakecov-option-description");
// questionResponse
expect(prompt).toContain("wakecov-question-interaction-id");
expect(prompt).toContain("wakecov-question-summary");
// executionWorkspace
expect(prompt).toContain("wakecov-branch-name");
// agentMessage
expect(prompt).toContain("wakecov-plugin-key");
expect(prompt).toContain("wakecov-agent-message-text");
// childIssueSummaries + childIssueSummaryTruncated
expect(prompt).toContain("wakecov-child-identifier wakecov-child-title (wakecov-child-status)");
expect(prompt).toContain("wakecov-child-summary");
expect(prompt).toContain("[child issue summaries truncated]");
// comments, commentWindow (requestedCount/includedCount), latestCommentId, missingCount
expect(prompt).toContain("wakecov-comment-id");
expect(prompt).toContain("wakecov-comment-body");
expect(prompt).toContain("- pending comments: 1/3");
expect(prompt).toContain("- latest comment id: wakecov-latest-comment-id");
expect(prompt).toContain("- omitted comments: 2");
// fallbackFetchNeeded
expect(prompt).toContain("- fallback fetch needed: yes");
});
it("the prompt carries the resume delta on a resumed session", () => {
const payload = {
reason: "issue_commented",
issue: { id: "wakecov-resume-issue-id", identifier: "PAP-9100", title: "Resume delta coverage" },
executionContinuation: {
version: 1,
companyId: "wakecov-company-id",
issueId: "wakecov-resume-issue-id",
trigger: { reason: "issue_commented", interactionId: null, sourceRunId: null },
originCommentIds: [],
objective: "wakecov-resume-objective",
messages: [
{
id: "wakecov-full-message-id",
authorType: "user",
authorId: "wakecov-message-author",
body: "wakecov-full-history-message",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
deleted: false,
sourceTrust: "trusted",
},
],
interactionOutcomes: [],
completedWork: null,
resumeDelta: {
baseRunId: "wakecov-base-run-id",
messages: [
{
id: "wakecov-delta-message-id",
authorType: "user",
authorId: "wakecov-message-author",
body: "wakecov-delta-only-message",
createdAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
deleted: false,
sourceTrust: "trusted",
},
],
},
unresolvedInteractionIds: [],
coverage: { kind: "full_task_history", throughCommentId: null, summaryThroughCommentId: null },
},
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
comments: [],
fallbackFetchNeeded: false,
};
const prompt = renderPaperclipWakePrompt(payload, { resumedSession: true });
expect(prompt).toContain("wakecov-delta-only-message");
expect(prompt).toContain("task_history_delta");
expect(prompt).toContain("wakecov-base-run-id");
expect(prompt).not.toContain("wakecov-full-history-message");
});
});
describe("WATCHDOG_DEFAULT_MANDATE", () => {

View File

@ -48,7 +48,6 @@ import {
selectPaperclipTaskMarkdown,
rewriteWorkspaceCwdEnvVarsForExecution,
shapePaperclipWorkspaceEnvForExecution,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
} from "@paperclipai/adapter-utils/server-utils";
import { buildSkillLibraryManifestMarkdown } from "@paperclipai/adapter-utils/skill-library-manifest";
@ -240,7 +239,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) {
@ -264,9 +262,6 @@ 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;
}
applyPaperclipWorkspaceEnv(env, {
workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
workspaceSource,

View File

@ -46,7 +46,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
joinPromptSections,
} from "@paperclipai/adapter-utils/server-utils";
@ -897,7 +896,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -920,9 +918,6 @@ 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;
}
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -22,7 +22,6 @@ import {
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
renderTemplate,
stringifyPaperclipWakePayload,
} from "@paperclipai/adapter-utils/server-utils";
type CursorCloudSession = {
@ -122,7 +121,6 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -131,7 +129,6 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
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 (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
if (authToken) {
env.PAPERCLIP_API_KEY = authToken;

View File

@ -45,7 +45,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
joinPromptSections,
} from "@paperclipai/adapter-utils/server-utils";
@ -270,7 +269,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -293,9 +291,6 @@ 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;
}
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -48,7 +48,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -294,7 +293,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
@ -303,7 +301,6 @@ 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;
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -36,7 +36,6 @@ import {
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
resolveLegacyPaperclipDesiredSkillNames,
stringifyPaperclipWakePayload,
refreshPaperclipWorkspaceEnvForExecution,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
} from "@paperclipai/adapter-utils/server-utils";
@ -281,7 +280,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value: unknown): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
@ -290,7 +288,6 @@ 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;
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -506,8 +506,6 @@ export async function execute(
if (envWakeReason) env.PAPERCLIP_WAKE_REASON = envWakeReason;
const envCommentId = cfgString(ctxContext.commentId) || cfgString(ctxContext.wakeCommentId) || cfgString(ctx.config?.commentId);
if (envCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = envCommentId;
const wakePayloadJson = stringifyPaperclipWakePayload(ctxContext.paperclipWake);
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
// ── Resolve working directory ──────────────────────────────────────────
const cwd =

View File

@ -40,7 +40,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
} from "@paperclipai/adapter-utils/server-utils";
import {
@ -267,7 +266,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
@ -276,7 +274,6 @@ 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;
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -41,7 +41,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
runChildProcess,
isPaperclipSkillSourceMissing,
@ -293,7 +292,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
@ -302,7 +300,6 @@ 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;
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -46,7 +46,6 @@ import {
renderTemplate,
renderPaperclipWakePrompt,
isPaperclipRecoveryWakePayload,
stringifyPaperclipWakePayload,
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -298,7 +297,6 @@ 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);
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -308,7 +306,6 @@ 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;
refreshPaperclipWorkspaceEnvForExecution({
env,
envConfig,

View File

@ -315,8 +315,8 @@ The runner, package driver, and model/harness never receive:
- the local agent JWT, `PAPERCLIP_API_KEY`, a board session, or a board API key;
- managed MCP gateway credentials, runner-lease/bootstrap credentials, or
credential-broker secret material;
- `PAPERCLIP_WAKE_PAYLOAD_JSON`, rendered Paperclip wake text, Paperclip skill
instructions, the Paperclip API manual, or run-scoped skill material;
- rendered Paperclip wake text, Paperclip skill instructions, the Paperclip
API manual, or run-scoped skill material;
- raw `process.env`, agent/project/routine env maps, `runtimeConfig.env`, or the
legacy adapter's generic execution context;
- authority to choose a company, issue, agent, policy, approval, or status;

View File

@ -624,19 +624,18 @@ describe("codex execute", () => {
expect(result.errorMessage).toBeNull();
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(capture.paperclipWakePayloadJson).not.toBeNull();
expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({
reason: "issue_commented",
latestCommentId: "comment-2",
commentIds: ["comment-1", "comment-2"],
});
// The prompt is the only delivery path for the wake payload now; the
// launch env no longer carries a second copy.
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(capture.paperclipWakePayloadJson).toBeNull();
expect(capture.prompt).toContain("## Paperclip Wake Payload");
expect(capture.prompt).toContain("Treat this wake payload as the highest-priority change for the current heartbeat.");
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
expect(capture.prompt).toContain(
"acknowledge the latest comment and explain how it changes your next action.",
);
expect(capture.prompt).toContain("- reason: issue_commented");
expect(capture.prompt).toContain("- latest comment id: comment-2");
expect(capture.prompt).toContain("First comment");
expect(capture.prompt).toContain("Second comment");
} finally {
@ -646,6 +645,95 @@ describe("codex execute", () => {
}
});
it("the launch env omits PAPERCLIP_WAKE_PAYLOAD_JSON", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-wake-env-"));
const workspace = path.join(root, "workspace");
const commandPath = path.join(root, "codex");
const capturePath = path.join(root, "capture.json");
await fs.mkdir(workspace, { recursive: true });
await writeFakeCodexCommand(commandPath);
const previousHome = process.env.HOME;
process.env.HOME = root;
await seedSharedCodexAuth(root);
try {
const result = await execute({
runId: "run-wake-env",
agent: {
id: "agent-1",
companyId: "company-1",
name: "Codex Coder",
adapterType: "codex_local",
adapterConfig: { engine: "cli" },
},
runtime: {
sessionId: null,
sessionParams: null,
sessionDisplayId: null,
taskKey: null,
},
config: {
engine: "cli",
command: commandPath,
cwd: workspace,
env: {
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
},
promptTemplate: "Follow the paperclip heartbeat.",
},
context: {
issueId: "issue-1",
taskId: "issue-1",
wakeReason: "issue_commented",
wakeCommentId: "comment-2",
paperclipWake: {
reason: "issue_commented",
issue: {
id: "issue-1",
identifier: "PAP-874",
title: "chat-speed issues",
status: "in_progress",
priority: "medium",
},
commentIds: ["comment-2"],
latestCommentId: "comment-2",
comments: [
{
id: "comment-2",
issueId: "issue-1",
body: "Second comment",
bodyTruncated: false,
createdAt: "2026-03-28T14:35:10.000Z",
author: { type: "user", id: "user-1" },
},
],
commentWindow: {
requestedCount: 1,
includedCount: 1,
missingCount: 0,
},
truncated: false,
fallbackFetchNeeded: false,
},
},
authToken: "run-jwt-token",
onLog: async () => {},
});
expect(result.exitCode).toBe(0);
expect(result.errorMessage).toBeNull();
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(capture.paperclipWakePayloadJson).toBeNull();
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
await fs.rm(root, { recursive: true, force: true });
}
});
it("classifies remote-compaction high-demand failures as retryable transient upstream errors", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-transient-"));
const workspace = path.join(root, "workspace");
@ -1181,26 +1269,19 @@ process.exit(1);
expect(result.errorMessage).toBeNull();
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(capture.paperclipWakePayloadJson).not.toBeNull();
expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({
reason: "issue_assigned",
issue: {
identifier: "PAP-1201",
title: "Fix gallery opening for inline images",
status: "in_progress",
priority: "medium",
},
checkedOutByHarness: true,
commentIds: [],
});
// The prompt is the only delivery path for the wake payload now; the
// launch env no longer carries a second copy.
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
expect(capture.paperclipWakePayloadJson).toBeNull();
expect(capture.prompt).toContain("## Paperclip Wake Payload");
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
expect(capture.prompt).toContain("- reason: issue_assigned");
expect(capture.prompt).toContain("- issue: PAP-1201 Fix gallery opening for inline images");
expect(capture.prompt).toContain("- issue status: in_progress");
expect(capture.prompt).toContain("- issue priority: medium");
expect(capture.prompt).not.toContain("- pending comments:");
expect(capture.prompt).not.toContain("acknowledge the latest comment");
expect(capture.prompt).not.toContain("Execution contract:");
expect(capture.prompt).toContain("- issue status: in_progress");
expect(capture.prompt).toContain("- checkout: already claimed by the harness for this run");
expect(capture.prompt).toContain("The harness already checked out this issue for the current run.");
} finally {

View File

@ -19,7 +19,7 @@ In Paperclip, **task** and **issue** refer to the same work item. The UI may use
Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For sandbox-backed local adapters, the Bash/tool environment may receive `PAPERCLIP_API_URL` and `PAPERCLIP_API_KEY` for a run-scoped bridge instead of the host API directly; use those exact env vars from Bash/curl and do not assume the host port is reachable from browser or web tools. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL, and never paste the API key or bridge token into prompts, comments, documents, restored workspace files, or logs.
Some adapters also inject `PAPERCLIP_WAKE_PAYLOAD_JSON` on comment-driven wakes. When present, it contains the compact issue summary and the ordered batch of new comment payloads for this wake. Use it first. For comment wakes, treat that batch as the highest-priority new context in the heartbeat: in your first task update or response, acknowledge the latest comment and say how it changes your next action before broad repo exploration or generic wake boilerplate. Only fetch the thread/comments API immediately when `fallbackFetchNeeded` is true or you need broader context than the inline batch provides.
The wake payload is part of this prompt, not a separate environment variable. Look for the `## Paperclip Wake Payload` or `## Paperclip Resume Delta` heading above. It holds the compact issue summary and the ordered batch of new comments for this wake. Use it first. For comment wakes, treat that batch as the highest-priority context in the heartbeat. In your first task update, acknowledge the latest comment. State how it changes your next action. Do this before broad repo exploration or generic wake text. Fetch the thread or comments API only when `fallbackFetchNeeded` is true. Also fetch it when you need more context than the inline batch gives.
Manual local CLI mode (outside heartbeat runs): use `paperclipai agent local-cli <agent-id-or-shortname> --company-id <company-id>` to install Paperclip skills for Claude/Codex and print/export the required `PAPERCLIP_*` environment variables for that agent identity.
@ -110,7 +110,7 @@ If already checked out by you, returns normally. If owned by another agent: `409
**Step 6 — Understand context.** Prefer `GET /api/issues/{issueId}/heartbeat-context` first. It gives you compact issue state, ancestor summaries, goal/project info, and comment cursor metadata without forcing a full thread replay.
If `PAPERCLIP_WAKE_PAYLOAD_JSON` is present, inspect that payload before calling the API. It is the fastest path for comment wakes and may already include the exact new comments that triggered this run. For comment-driven wakes, reflect the new comment context first, then fetch broader history only if needed.
Read the wake payload block of this prompt before you call the API. Look for the `## Paperclip Wake Payload` or `## Paperclip Resume Delta` heading. It is the fastest path for comment wakes. It may already hold the exact new comments that triggered this run. For comment-driven wakes, reflect the new comment context first. Then fetch broader history only if you need it.
Use comments incrementally: