Preserve chat policy across fresh and resumed provider sessions

This commit is contained in:
Dotta committed 2026-09-11 16:15:56 -05:00
1 parent 3556fa25f1
commit abacbdfd2f
8 files changed
+114 -19

No files matched your search

@@ -4,6 +4,8 @@ import os from "node:os";
import path from "node:path";
import { runChildProcess } from "@paperclipai/adapter-utils/server-utils";
import { execute } from "@paperclipai/adapter-codex-local/server";
import { buildPaperclipTaskMarkdown } from "../services/heartbeat.js";
import { AGENT_CHAT_DIRECTIVE } from "../services/agent-conversations.js";
async function writeFakeCodexCommand(commandPath: string): Promise<void> {
const script = `#!/usr/bin/env node
@@ -1210,7 +1212,7 @@ process.exit(1);
}
});
it("uses a compact wake delta instead of the full heartbeat prompt when resuming a session", async () => {
it.each([{ conversationMode: false, resumedSession: true }, { conversationMode: true, resumedSession: true }, { conversationMode: true, resumedSession: false }])("retains current task policy (conversation=$conversationMode, resumed=$resumedSession)", async ({ conversationMode, resumedSession }) => {
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");
@@ -1224,6 +1226,14 @@ process.exit(1);
process.env.HOME = root;
await seedSharedCodexAuth(root);
const policy = conversationMode
? buildPaperclipTaskMarkdown({
issue: { id: "issue-1", title: "Chat", workMode: "planning", conversationAgentId: "agent-1" },
interaction: { kind: "request_confirmation", status: "rejected" },
planReview: { status: "rejected", reason: "Revise the final note." },
includeDescription: false,
})
: "Current ordinary task policy";
let invocationPrompt = "";
let invocationNotes: string[] = [];
let promptMetrics: Record<string, number> = {};
@@ -1240,7 +1250,7 @@ process.exit(1);
runtime: {
sessionId: null,
sessionParams: {
sessionId: "codex-session-1",
sessionId: resumedSession ? "codex-session-1" : null,
cwd: workspace,
},
sessionDisplayId: null,
@@ -1254,9 +1264,12 @@ process.exit(1);
env: {
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
},
promptTemplate: "Follow the paperclip heartbeat.",
promptTemplate: conversationMode ? undefined : "Follow the paperclip heartbeat.",
},
context: {
conversationMode,
paperclipTaskMarkdown: `Full description that must not replay\n${policy}`,
paperclipTaskMarkdownCompact: policy,
issueId: "issue-1",
taskId: "issue-1",
wakeReason: "issue_commented",
@@ -1304,18 +1317,35 @@ process.exit(1);
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");
if (resumedSession) expect(capture.argv).toEqual(expect.arrayContaining(["resume", "codex-session-1", "-"]));
else expect(capture.argv).not.toContain("resume");
expect(capture.prompt).toContain(resumedSession ? "## Paperclip Resume Delta" : "## Paperclip Wake Payload");
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
expect(capture.prompt).toContain("Second comment");
expect(capture.prompt).toContain(policy);
expect(invocationPrompt).toContain(policy);
if (resumedSession) expect(capture.prompt).not.toContain("Full description that must not replay");
else expect(capture.prompt).toContain("Full description that must not replay");
expect(promptMetrics.taskContextChars).toBe(resumedSession ? policy.length : `Full description that must not replay\n${policy}`.length);
if (conversationMode) {
expect(invocationPrompt).toContain(AGENT_CHAT_DIRECTIVE);
expect(invocationPrompt).toContain("baseRevisionId set to that latestRevisionId");
expect(capture.prompt).not.toContain("Execution contract:");
expect(capture.prompt).not.toContain("Use child issues");
} else {
expect(capture.prompt).toContain("Execution contract:");
}
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);
if (resumedSession) {
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);
} else {
expect(capture.prompt).toContain("You are managed instructions.");
expect(promptMetrics.heartbeatPromptChars).toBeGreaterThan(0);
}
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
+2 -1
View File
@@ -20248,7 +20248,7 @@ export function heartbeatService(
...nativeExecution,
task: {
...nativeExecution.task,
prompt: `${nativeExecution.task.prompt}\n\n${renderPaperclipWakePrompt({ executionContinuation }, { resumedSession: true })}`,
prompt: `${nativeExecution.task.prompt}\n\n${renderPaperclipWakePrompt({ executionContinuation }, { resumedSession: true, conversationMode: context.conversationMode === true })}`,
},
});
}
@@ -20376,6 +20376,7 @@ export function heartbeatService(
`# ${issueRef.identifier ?? issueRef.id}: ${issueRef.title}`,
wakePayload: context.paperclipWake,
resumedSession: previousNativeRun !== null,
conversationMode: context.conversationMode === true,
agentId: agent.id,
workspace: {
// Projectless paperclip_runner tasks still have a resolved local cwd. Bind that
@@ -35,6 +35,7 @@ export function buildNativeExecutionInput(input: {
*/
wakePayload?: unknown;
resumedSession?: boolean;
conversationMode?: boolean;
agentId: string;
workspace: {
id: string;
@@ -89,6 +90,7 @@ export function buildNativeExecutionInput(input: {
: null;
const wakePrompt = renderPaperclipWakePrompt(input.wakePayload, {
resumedSession: input.resumedSession === true,
conversationMode: input.conversationMode === true,
suppressIssueDescription: input.taskPrompt.trim().length > 0,
});
const taskPrompt = [wakePrompt, input.taskPrompt.trim()]
@@ -405,7 +405,7 @@ describe("buildNativeExecutionInput wake projection", () => {
.not.toMatch(/OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_SECRET_ACCESS_KEY|PAPERCLIP_API_KEY/);
});
it("places child completion summaries in the closed provider prompt", () => {
it.each([false, true])("projects wake context with the appropriate execution contract (conversation=%s)", (conversationMode) => {
const input = buildNativeExecutionInput({
companyId,
runId: currentRunId,
@@ -439,6 +439,7 @@ describe("buildNativeExecutionInput wake projection", () => {
checkedOutByHarness: true,
},
resumedSession: true,
conversationMode,
agentId,
workspace: {
id: currentRunId,
@@ -463,6 +464,8 @@ describe("buildNativeExecutionInput wake projection", () => {
runtimeContext: nativeRuntimeContextFixture(),
});
expect(input.task.prompt.includes("Execution contract:")).toBe(!conversationMode);
expect(input.task.prompt.includes("Use child issues")).toBe(!conversationMode);
expect(input.task.prompt).toContain("## Paperclip Resume Delta");
expect(input.task.prompt).toContain("reason: issue_children_completed");
expect(input.task.prompt).toContain("DOT-147 Build utility (done)");