diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index e9c4f0a576..abbd0256e7 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -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"); diff --git a/packages/adapter-utils/src/acpx-engine/execute.ts b/packages/adapter-utils/src/acpx-engine/execute.ts index 3f537fedd1..926dc8800d 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.ts @@ -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, diff --git a/packages/adapter-utils/src/execution-target-sandbox.test.ts b/packages/adapter-utils/src/execution-target-sandbox.test.ts index ce1d9f6faf..06c1ffda0b 100644 --- a/packages/adapter-utils/src/execution-target-sandbox.test.ts +++ b/packages/adapter-utils/src/execution-target-sandbox.test.ts @@ -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[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 }, diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index dc9c99d1ec..39ddf0a752 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -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", () => { diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 02230de87a..9540986482 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -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 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 0) { env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); } - if (wakePayloadJson) { - env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - } applyPaperclipWorkspaceEnv(env, { workspaceCwd: shapedWorkspaceEnv.workspaceCwd, workspaceSource, diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 7a09fc0489..62ac49badf 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -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 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 0) { env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); } - if (wakePayloadJson) { - env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - } refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/cursor-cloud/src/server/execute.ts b/packages/adapters/cursor-cloud/src/server/execute.ts index b3300e50b6..ec173fa6c9 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.ts @@ -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 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 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; diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index eed1a926a5..71e6d7f5ab 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -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 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 0) { env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); } - if (wakePayloadJson) { - env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - } refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index 5ecfd95b43..2b52c01b35 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -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 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 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/grok-local/src/server/execute.ts b/packages/adapters/grok-local/src/server/execute.ts index 98880c00fe..27bfc23dbf 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -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 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 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/hermes/src/server/execute.ts b/packages/adapters/hermes/src/server/execute.ts index 021ad08dd8..119ec296ae 100644 --- a/packages/adapters/hermes/src/server/execute.ts +++ b/packages/adapters/hermes/src/server/execute.ts @@ -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 = diff --git a/packages/adapters/kimi-local/src/server/execute.ts b/packages/adapters/kimi-local/src/server/execute.ts index f9be53cc95..37f7e6f051 100644 --- a/packages/adapters/kimi-local/src/server/execute.ts +++ b/packages/adapters/kimi-local/src/server/execute.ts @@ -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 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 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index dba49d84c8..005a2ad418 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -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 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 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 08f80fb991..4b1621c96b 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -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 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 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, diff --git a/packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md b/packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md index 82f3a43432..a62c4d6bdf 100644 --- a/packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md +++ b/packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md @@ -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; diff --git a/server/src/__tests__/codex-local-execute.test.ts b/server/src/__tests__/codex-local-execute.test.ts index 841b7d6fa7..2a9d47b721 100644 --- a/server/src/__tests__/codex-local-execute.test.ts +++ b/server/src/__tests__/codex-local-execute.test.ts @@ -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 { diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index ac5e26a1cb..94b6f3573c 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -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 --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: