From b9b2bf3b5b9d9a31a271488bf1545b6656f583a2 Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 28 Mar 2026 10:33:40 -0500 Subject: [PATCH] Trim resumed comment wake prompts --- packages/adapter-utils/src/server-utils.ts | 48 +++++--- .../claude-local/src/server/execute.ts | 5 +- .../codex-local/src/server/execute.ts | 44 ++++--- .../cursor-local/src/server/execute.ts | 5 +- .../gemini-local/src/server/execute.ts | 5 +- .../opencode-local/src/server/execute.ts | 6 +- .../adapters/pi-local/src/server/execute.ts | 5 +- .../src/__tests__/codex-local-execute.test.ts | 110 ++++++++++++++++++ .../__tests__/gemini-local-execute.test.ts | 96 +++++++++++++++ 9 files changed, 282 insertions(+), 42 deletions(-) diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index d6e67c3b..1d84f776 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -293,24 +293,42 @@ export function stringifyPaperclipWakePayload(value: unknown): string | null { return JSON.stringify(normalized); } -export function renderPaperclipWakePrompt(value: unknown): string { +export function renderPaperclipWakePrompt( + value: unknown, + options: { resumedSession?: boolean } = {}, +): string { const normalized = normalizePaperclipWakePayload(value); if (!normalized) return ""; + const resumedSession = options.resumedSession === true; - const lines = [ - "## Paperclip Wake Payload", - "", - "Treat this wake payload as the highest-priority change for the current heartbeat.", - "Before generic repo exploration or boilerplate heartbeat updates, acknowledge the latest comment and explain how it changes your next action.", - "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"}`, - ]; + const lines = resumedSession + ? [ + "## Paperclip Resume Delta", + "", + "You are resuming an existing Paperclip session.", + "Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate.", + "Fetch the API thread only 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"}`, + ] + : [ + "## Paperclip Wake Payload", + "", + "Treat this wake payload as the highest-priority change for the current heartbeat.", + "Before generic repo exploration or boilerplate heartbeat updates, acknowledge the latest comment and explain how it changes your next action.", + "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}`); diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 072cdf4c..eca82b50 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -404,12 +404,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? renderTemplate(bootstrapPromptTemplate, templateData).trim() : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const prompt = joinPromptSections([ renderedBootstrapPrompt, diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 63a31a1b..c6ba3e9b 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -440,11 +440,36 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 + ? renderTemplate(bootstrapPromptTemplate, templateData).trim() + : ""; + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix; + instructionsChars = promptInstructionsPrefix.length; const commandNotes = (() => { if (!instructionsFilePath) { return [repoAgentsNote]; } if (instructionsPrefix.length > 0) { + if (shouldUseResumeDeltaPrompt) { + return [ + `Loaded agent instructions from ${instructionsFilePath}`, + "Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.", + repoAgentsNote, + ]; + } return [ `Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`, @@ -456,25 +481,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 - ? renderTemplate(bootstrapPromptTemplate, templateData).trim() - : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const prompt = joinPromptSections([ - instructionsPrefix, + promptInstructionsPrefix, renderedBootstrapPrompt, wakePrompt, sessionHandoffNote, diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index ded063c2..648ef72c 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -358,12 +358,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? renderTemplate(bootstrapPromptTemplate, templateData).trim() : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const paperclipEnvNote = renderPaperclipEnvNote(env); const prompt = joinPromptSections([ diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index aaa41a2a..95e9ff67 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -299,12 +299,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? renderTemplate(bootstrapPromptTemplate, templateData).trim() : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const paperclipEnvNote = renderPaperclipEnvNote(env); const apiAccessNote = renderApiAccessNote(env); diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 99241d7f..7651578f 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -227,6 +227,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? renderTemplate(bootstrapPromptTemplate, templateData).trim() : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const prompt = joinPromptSections([ instructionsPrefix, diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index cd38db5a..c7eb61a2 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -302,12 +302,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? renderTemplate(bootstrapPromptTemplate, templateData).trim() : ""; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake); + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession }); + const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0; + const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); const userPrompt = joinPromptSections([ renderedBootstrapPrompt, diff --git a/server/src/__tests__/codex-local-execute.test.ts b/server/src/__tests__/codex-local-execute.test.ts index 795a0ca3..8c6d80f7 100644 --- a/server/src/__tests__/codex-local-execute.test.ts +++ b/server/src/__tests__/codex-local-execute.test.ts @@ -368,6 +368,116 @@ describe("codex execute", () => { } }); + it("uses a compact wake delta instead of the full heartbeat prompt when resuming a session", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-resume-wake-")); + const workspace = path.join(root, "workspace"); + const commandPath = path.join(root, "codex"); + const capturePath = path.join(root, "capture.json"); + const instructionsPath = path.join(root, "AGENTS.md"); + await fs.mkdir(workspace, { recursive: true }); + await fs.writeFile(instructionsPath, "You are managed instructions.\n", "utf8"); + await writeFakeCodexCommand(commandPath); + + const previousHome = process.env.HOME; + process.env.HOME = root; + + let invocationPrompt = ""; + let invocationNotes: string[] = []; + let promptMetrics: Record = {}; + try { + const result = await execute({ + runId: "run-resume-wake", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Codex Coder", + adapterType: "codex_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: { + sessionId: "codex-session-1", + cwd: workspace, + }, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + instructionsFilePath: instructionsPath, + 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 () => {}, + onMeta: async (meta) => { + invocationPrompt = meta.prompt ?? ""; + invocationNotes = meta.commandNotes ?? []; + promptMetrics = meta.promptMetrics ?? {}; + }, + }); + + expect(result.exitCode).toBe(0); + expect(result.errorMessage).toBeNull(); + + const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; + expect(capture.argv).toEqual(expect.arrayContaining(["resume", "codex-session-1", "-"])); + expect(capture.prompt).toContain("## Paperclip Resume Delta"); + expect(capture.prompt).toContain("Second comment"); + expect(capture.prompt).not.toContain("Follow the paperclip heartbeat."); + expect(capture.prompt).not.toContain("You are managed instructions."); + expect(invocationPrompt).toContain("## Paperclip Resume Delta"); + expect(invocationNotes).toContain( + "Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.", + ); + expect(promptMetrics.instructionsChars).toBe(0); + expect(promptMetrics.heartbeatPromptChars).toBe(0); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("uses a worktree-isolated CODEX_HOME while preserving shared auth and config", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-")); const workspace = path.join(root, "workspace"); diff --git a/server/src/__tests__/gemini-local-execute.test.ts b/server/src/__tests__/gemini-local-execute.test.ts index 06fdaf03..d8b1cb9c 100644 --- a/server/src/__tests__/gemini-local-execute.test.ts +++ b/server/src/__tests__/gemini-local-execute.test.ts @@ -168,4 +168,100 @@ describe("gemini execute", () => { await fs.rm(root, { recursive: true, force: true }); } }); + + it("uses a compact wake delta instead of the full heartbeat prompt when resuming a session", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-resume-wake-")); + const workspace = path.join(root, "workspace"); + const commandPath = path.join(root, "gemini"); + const capturePath = path.join(root, "capture.json"); + await fs.mkdir(workspace, { recursive: true }); + await writeFakeGeminiCommand(commandPath); + + const previousHome = process.env.HOME; + process.env.HOME = root; + + try { + const result = await execute({ + runId: "run-resume", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Gemini Coder", + adapterType: "gemini_local", + adapterConfig: {}, + }, + runtime: { + sessionId: "gemini-session-1", + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + model: "gemini-2.5-pro", + 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; + const promptFlagIndex = capture.argv.indexOf("--prompt"); + const promptArg = promptFlagIndex >= 0 ? capture.argv[promptFlagIndex + 1] : ""; + expect(capture.argv).toContain("--resume"); + expect(capture.argv).toContain("gemini-session-1"); + expect(promptArg).toContain("## Paperclip Resume Delta"); + expect(promptArg).toContain("Second comment"); + expect(promptArg).not.toContain("Follow the paperclip heartbeat."); + } finally { + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + await fs.rm(root, { recursive: true, force: true }); + } + }); });