mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Trim resumed comment wake prompts
This commit is contained in:
parent
4dea302791
commit
b9b2bf3b5b
9 changed files with 282 additions and 42 deletions
|
|
@ -293,24 +293,42 @@ export function stringifyPaperclipWakePayload(value: unknown): string | null {
|
||||||
return JSON.stringify(normalized);
|
return JSON.stringify(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPaperclipWakePrompt(value: unknown): string {
|
export function renderPaperclipWakePrompt(
|
||||||
|
value: unknown,
|
||||||
|
options: { resumedSession?: boolean } = {},
|
||||||
|
): string {
|
||||||
const normalized = normalizePaperclipWakePayload(value);
|
const normalized = normalizePaperclipWakePayload(value);
|
||||||
if (!normalized) return "";
|
if (!normalized) return "";
|
||||||
|
const resumedSession = options.resumedSession === true;
|
||||||
|
|
||||||
const lines = [
|
const lines = resumedSession
|
||||||
"## Paperclip Wake Payload",
|
? [
|
||||||
"",
|
"## Paperclip Resume Delta",
|
||||||
"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.",
|
"You are resuming an existing Paperclip session.",
|
||||||
"Use this inline wake data first before refetching the issue thread.",
|
"Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate.",
|
||||||
"Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch.",
|
"Fetch the API thread only when `fallbackFetchNeeded` is true or you need broader history than this batch.",
|
||||||
"",
|
"",
|
||||||
`- reason: ${normalized.reason ?? "unknown"}`,
|
`- reason: ${normalized.reason ?? "unknown"}`,
|
||||||
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
||||||
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
||||||
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
|
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
|
||||||
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
|
`- 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) {
|
if (normalized.issue?.status) {
|
||||||
lines.push(`- issue status: ${normalized.issue.status}`);
|
lines.push(`- issue status: ${normalized.issue.status}`);
|
||||||
|
|
|
||||||
|
|
@ -404,12 +404,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
run: { id: runId, source: "on_demand" },
|
run: { id: runId, source: "on_demand" },
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
const renderedBootstrapPrompt =
|
||||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
? 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 sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const prompt = joinPromptSections([
|
const prompt = joinPromptSections([
|
||||||
renderedBootstrapPrompt,
|
renderedBootstrapPrompt,
|
||||||
|
|
|
||||||
|
|
@ -440,11 +440,36 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
}
|
}
|
||||||
const repoAgentsNote =
|
const repoAgentsNote =
|
||||||
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery.";
|
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery.";
|
||||||
|
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
||||||
|
const templateData = {
|
||||||
|
agentId: agent.id,
|
||||||
|
companyId: agent.companyId,
|
||||||
|
runId,
|
||||||
|
company: { id: agent.companyId },
|
||||||
|
agent,
|
||||||
|
run: { id: runId, source: "on_demand" },
|
||||||
|
context,
|
||||||
|
};
|
||||||
|
const renderedBootstrapPrompt =
|
||||||
|
!sessionId && bootstrapPromptTemplate.trim().length > 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 = (() => {
|
const commandNotes = (() => {
|
||||||
if (!instructionsFilePath) {
|
if (!instructionsFilePath) {
|
||||||
return [repoAgentsNote];
|
return [repoAgentsNote];
|
||||||
}
|
}
|
||||||
if (instructionsPrefix.length > 0) {
|
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 [
|
return [
|
||||||
`Loaded agent instructions from ${instructionsFilePath}`,
|
`Loaded agent instructions from ${instructionsFilePath}`,
|
||||||
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
||||||
|
|
@ -456,25 +481,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
repoAgentsNote,
|
repoAgentsNote,
|
||||||
];
|
];
|
||||||
})();
|
})();
|
||||||
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||||
const templateData = {
|
|
||||||
agentId: agent.id,
|
|
||||||
companyId: agent.companyId,
|
|
||||||
runId,
|
|
||||||
company: { id: agent.companyId },
|
|
||||||
agent,
|
|
||||||
run: { id: runId, source: "on_demand" },
|
|
||||||
context,
|
|
||||||
};
|
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
|
||||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
|
||||||
: "";
|
|
||||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake);
|
|
||||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const prompt = joinPromptSections([
|
const prompt = joinPromptSections([
|
||||||
instructionsPrefix,
|
promptInstructionsPrefix,
|
||||||
renderedBootstrapPrompt,
|
renderedBootstrapPrompt,
|
||||||
wakePrompt,
|
wakePrompt,
|
||||||
sessionHandoffNote,
|
sessionHandoffNote,
|
||||||
|
|
|
||||||
|
|
@ -358,12 +358,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
run: { id: runId, source: "on_demand" },
|
run: { id: runId, source: "on_demand" },
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
const renderedBootstrapPrompt =
|
||||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
? 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 sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||||
const prompt = joinPromptSections([
|
const prompt = joinPromptSections([
|
||||||
|
|
|
||||||
|
|
@ -299,12 +299,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
run: { id: runId, source: "on_demand" },
|
run: { id: runId, source: "on_demand" },
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
const renderedBootstrapPrompt =
|
||||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
? 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 sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||||
const apiAccessNote = renderApiAccessNote(env);
|
const apiAccessNote = renderApiAccessNote(env);
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||||
const resolvedInstructionsFilePath = instructionsFilePath
|
const resolvedInstructionsFilePath = instructionsFilePath
|
||||||
? path.resolve(cwd, instructionsFilePath)
|
? path.resolve(cwd, instructionsFilePath)
|
||||||
|
|
@ -275,12 +276,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
run: { id: runId, source: "on_demand" },
|
run: { id: runId, source: "on_demand" },
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
const renderedBootstrapPrompt =
|
||||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
? 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 sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const prompt = joinPromptSections([
|
const prompt = joinPromptSections([
|
||||||
instructionsPrefix,
|
instructionsPrefix,
|
||||||
|
|
|
||||||
|
|
@ -302,12 +302,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
|
const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
|
||||||
const renderedHeartbeatPrompt = renderTemplate(promptTemplate, templateData);
|
|
||||||
const renderedBootstrapPrompt =
|
const renderedBootstrapPrompt =
|
||||||
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
|
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
|
||||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
? 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 sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||||
const userPrompt = joinPromptSections([
|
const userPrompt = joinPromptSections([
|
||||||
renderedBootstrapPrompt,
|
renderedBootstrapPrompt,
|
||||||
|
|
|
||||||
|
|
@ -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<string, number> = {};
|
||||||
|
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 () => {
|
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 root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-"));
|
||||||
const workspace = path.join(root, "workspace");
|
const workspace = path.join(root, "workspace");
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,100 @@ describe("gemini execute", () => {
|
||||||
await fs.rm(root, { recursive: true, force: true });
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue