diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index 9fe21551b31..0b52c22e171 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -629,6 +629,7 @@ describe("shared ACPX engine runtime behavior", () => { taskId: "chat-1", paperclipTaskMarkdown: chatDirective, paperclipTaskMarkdownCompact: chatDirective, + paperclipTaskCommunicationGuidance: "Frozen Slack communication preference.", paperclipWake: { reason: "issue_commented", issue: { id: "chat-1", workMode: "planning", status: "in_progress" }, @@ -647,6 +648,10 @@ describe("shared ACPX engine runtime behavior", () => { expect(resumed.sessionInputs[0]?.resumeSessionId).toBe(fresh.result.sessionId); const reset = await runExecutor(config, { context }); expect(reset.sessionInputs[0]?.resumeSessionId).toBeUndefined(); + for (const run of [fresh, reset]) { + expect(String(run.meta[0]?.prompt).match(/Frozen Slack communication preference\./g)).toHaveLength(1); + } + expect(String(resumed.meta[0]?.prompt)).not.toContain("Frozen Slack communication preference."); for (const { meta } of [fresh, resumed, reset]) { const prompt = String(meta[0]?.prompt ?? ""); expect(prompt).toContain(chatDirective); diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index e92916ea0c1..b694c871be7 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -26,6 +26,7 @@ import { resolveLegacyPaperclipDesiredSkillNames, resolvePaperclipDesiredSkillNames, selectPaperclipTaskMarkdown, + selectInitialCommunicationGuidance, runningProcesses, runChildProcess, sanitizeSshRemoteEnv, @@ -3061,6 +3062,9 @@ describe("selectPaperclipTaskMarkdown", () => { paperclipWake: wake("issue_commented"), }; expect(selectPaperclipTaskMarkdown(context)).toContain("Saved initial guidance"); + expect(selectInitialCommunicationGuidance({ paperclipTaskCommunicationGuidance: " Slack preference " })).toBe("Slack preference"); + expect(selectInitialCommunicationGuidance(context, { resumedSession: true })).toBe(""); + expect(selectInitialCommunicationGuidance({})).toBe(""); expect(selectPaperclipTaskMarkdown(context, { resumedSession: true })).toBe(compactMarkdown); expect(selectPaperclipTaskMarkdown(context, { includeCommunicationGuidance: false })).toBe(fullMarkdown); context.paperclipWake = { ...wake("issue_monitor_recovery"), recovery: { cause: "process_lost" } } as typeof context.paperclipWake; diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 33fde20be86..e45d008f8fe 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -2150,6 +2150,16 @@ export function isAssignmentShapedPaperclipWakeReason( ); } +// Select at the actual provider attempt boundary so a failed resume restores +// the original snapshot once when retrying with a fresh session. +export function selectInitialCommunicationGuidance( + context: Record | null | undefined, + options: { resumedSession?: boolean } = {}, +): string { + return options.resumedSession === true + ? "" : asString(context?.paperclipTaskCommunicationGuidance, "").trim(); +} + // Picks the task-context markdown variant for adapters that inject it into the // prompt. Fresh sessions, assignment-shaped wakes, and recovery wakes get the // full brief; other resume deltas get the compact variant (description @@ -2163,7 +2173,7 @@ export function selectPaperclipTaskMarkdown( if (!full) return ""; if (options.resumedSession !== true) { const guidance = options.includeCommunicationGuidance === false - ? "" : asString(context?.paperclipTaskCommunicationGuidance, "").trim(); + ? "" : selectInitialCommunicationGuidance(context, options); return joinPromptSections([guidance, full]); } const wake = normalizePaperclipWakePayload(context?.paperclipWake); diff --git a/packages/adapters/cursor-cloud/src/server/execute.ts b/packages/adapters/cursor-cloud/src/server/execute.ts index f157b55545e..f9dad078062 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.ts @@ -22,6 +22,7 @@ import { readPaperclipIssueWorkModeFromContext, renderPaperclipWakePrompt, selectPaperclipTaskMarkdown, + selectInitialCommunicationGuidance, isPaperclipRecoveryWakePayload, renderTemplate, stringifyPaperclipWakePayload, @@ -417,7 +418,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const prompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }), + basePrompt, + ]); const args = buildArgs(resumeSessionId); if (onMeta) { await onMeta({ @@ -625,7 +630,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const buildArgs = (resumeSessionId: string | null, prompt: string) => { const args = ["--output-format", "stream-json"]; if (resumeSessionId) args.push("--resume", resumeSessionId); if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model); @@ -611,7 +612,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - const args = buildArgs(resumeSessionId); + const prompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }), + basePrompt, + ]); + const args = buildArgs(resumeSessionId, prompt); const invocationEnv = buildGeminiHeadlessEnv(env); const invocationRuntimeEnv = buildGeminiRuntimeEnv(env); const loggedEnv = buildInvocationEnvForLogs(invocationEnv, { @@ -630,7 +635,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const buildArgs = (resumeSessionId: string | null, prompt: string) => { const args = ["--cwd", effectiveExecutionCwd, "--output-format", "streaming-json"]; if (resumeSessionId) args.push("--resume", resumeSessionId); if (model && model !== DEFAULT_GROK_LOCAL_MODEL) args.push("--model", model); @@ -544,7 +545,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - const args = buildArgs(resumeSessionId); + const prompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }), + basePrompt, + ]); + const args = buildArgs(resumeSessionId, prompt); if (onMeta) { await onMeta({ adapterType: "grok_local", @@ -556,7 +561,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const buildArgs = (resumeSessionId: string | null, prompt: string) => { const args = ["--output-format", "stream-json"]; if (resumeSessionId) args.push("-r", resumeSessionId); if (model) args.push("-m", model); @@ -577,7 +578,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - const args = buildArgs(resumeSessionId); + const prompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }), + basePrompt, + ]); + const args = buildArgs(resumeSessionId, prompt); const invocationEnv = buildKimiHeadlessEnv(env); const invocationRuntimeEnv = buildKimiRuntimeEnv(env); const loggedEnv = buildInvocationEnvForLogs(invocationEnv, { @@ -596,7 +601,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const prompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }), + basePrompt, + ]); const args = buildArgs(resumeSessionId); if (onMeta) { await onMeta({ @@ -633,7 +638,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise`], env: loggedEnv, prompt, - promptMetrics, + promptMetrics: { ...promptMetrics, promptChars: prompt.length }, context, }); } diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 5ed953cc7c0..4787d449952 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -46,6 +46,7 @@ import { renderTemplate, renderPaperclipWakePrompt, selectPaperclipTaskMarkdown, + selectInitialCommunicationGuidance, isPaperclipRecoveryWakePayload, stringifyPaperclipWakePayload, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, @@ -620,7 +621,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const buildArgs = (sessionFile: string, userPrompt: string): string[] => { const args: string[] = []; // Use JSON mode for structured output with print mode (non-interactive) @@ -692,7 +693,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - const args = buildArgs(sessionFile); + const userPrompt = joinPromptSections([ + selectInitialCommunicationGuidance(context, { resumedSession: canResumeSession && sessionFile === sessionPath }), + baseUserPrompt, + ]); + const args = buildArgs(sessionFile, userPrompt); if (onMeta) { await onMeta({ adapterType: "pi_local", @@ -702,7 +707,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { externalLabel: "Slack direct message", sessionGeneration: 1, isDirectMessage: true, + communicationGuidance: "## Communication in Slack\nFrozen connection preference.", state: "active", }); await db.insert(chatExternalPrincipals).values({ @@ -2030,6 +2031,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => { issueId, taskId: issueId, source: "chat:slack", + paperclipTaskCommunicationGuidance: "FORGED caller preference", commentId: sourceComment.id, wakeCommentId: sourceComment.id, wakeCommentIds: [sourceComment.id], @@ -2260,6 +2262,7 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => { wakeCommentId: sourceComment.id, wakeCommentIds: [sourceComment.id], paperclipExternalChatExecutionBound: true, + paperclipTaskCommunicationGuidance: "## Communication in Slack\nFrozen connection preference.", paperclipExternalChatQuestionResponse: expect.objectContaining({ schema: "paperclip.external_chat_question_response.v1", interactionId: answered.id, @@ -2273,6 +2276,9 @@ describeEmbeddedPostgres("heartbeat comment wake batching", () => { expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain( "preserve this full source instruction", ); + const restoredPrompt = String(gateway.getAgentPayloads()[0]?.message ?? ""); + expect(restoredPrompt.match(/Frozen connection preference\./g)).toHaveLength(1); + expect(restoredPrompt).not.toContain("FORGED caller preference"); expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain( "Preserve the original request's exact-output constraints literally.", );