FN-6375: include attachment paths in recovery prompts

Workflow context-limit recovery prompts now direct agents to read task attachments from the project root.

- Pass the project root into reduced workflow step prompts so attachment directories can be absolute when available.
- Replace the autonomous-agent "ask for context" wording with direct file-reading guidance for attachments.
- Cover root-aware, fallback, placement, and context-limit recovery attachment prompt behavior.
- Add a patch changeset for the published Fusion package.

Files changed:
 .../fn-6375-workflow-attachment-recovery-prompt.md |  5 ++
 .../src/__tests__/step-session-executor.test.ts    | 70 +++++++++++++++++++---
 packages/engine/src/step-session-executor.ts       |  8 ++-
 3 files changed, 72 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-6375
Fusion-Task-Lineage: ed9a5367-1623-49cb-9c7f-116c59ce5117
This commit is contained in:
gsxdsm
2026-06-13 12:26:01 -07:00
parent d1bea16f3a
commit ee6d7acec8
3 changed files with 72 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Workflow step execution now surfaces task attachment locations in the context-recovery prompt path and no longer tells autonomous agents to ask for context.

View File

@@ -732,7 +732,7 @@ describe("buildReducedStepPrompt", () => {
- [ ] Write unit tests
`;
it("includes one-line attachment reference when attachments exist", () => {
it("includes compact attachment location and read instruction when attachments exist", () => {
const task = makeTaskDetail({
id: "FN-123",
prompt: reducedPrompt,
@@ -754,11 +754,35 @@ describe("buildReducedStepPrompt", () => {
],
});
const result = buildReducedStepPrompt(task, 1);
const result = buildReducedStepPrompt(task, 1, "/repo/project");
expect(result).toContain(
"2 attachment(s) available at .fusion/tasks/FN-123/attachments/ — ask for context if needed.",
"2 attachment(s) available at `/repo/project/.fusion/tasks/FN-123/attachments/` — read the files there for context.",
);
expect(result).toContain("They live at the project root and are readable even when working in a worktree.");
expect(result).not.toContain("ask for context");
});
it("falls back to project-relative attachment location when rootDir is omitted", () => {
const task = makeTaskDetail({
id: "FN-123",
prompt: reducedPrompt,
attachments: [
{
filename: "abc-shot.png",
originalName: "shot.png",
mimeType: "image/png",
size: 1024,
createdAt: new Date().toISOString(),
},
],
});
const result = buildReducedStepPrompt(task, 1);
expect(result).toContain("1 attachment(s) available at `.fusion/tasks/FN-123/attachments/`");
expect(result).toContain("read the files there for context");
expect(result).not.toContain("ask for context");
});
it("places the attachment reference after the step and before the important block", () => {
@@ -775,9 +799,8 @@ describe("buildReducedStepPrompt", () => {
],
});
const result = buildReducedStepPrompt(task, 1);
const attachmentReference =
"1 attachment(s) available at .fusion/tasks/FN-001/attachments/ — ask for context if needed.";
const result = buildReducedStepPrompt(task, 1, "/repo/project");
const attachmentReference = "1 attachment(s) available at `/repo/project/.fusion/tasks/FN-001/attachments/`";
expect(result.indexOf(attachmentReference)).toBeGreaterThan(result.indexOf("Add exports"));
expect(result.indexOf(attachmentReference)).toBeLessThan(result.indexOf("IMPORTANT:"));
@@ -803,18 +826,49 @@ describe("buildReducedStepPrompt", () => {
expect(result).not.toContain("shot.png");
});
it("verifies context-limit recovery symptom is gone for image and non-image attachments", () => {
const task = makeTaskDetail({
id: "FN-456",
prompt: reducedPrompt,
attachments: [
{
filename: "abc-shot.png",
originalName: "shot.png",
mimeType: "image/png",
size: 1024,
createdAt: new Date().toISOString(),
},
{
filename: "def-config.json",
originalName: "config.json",
mimeType: "application/json",
size: 256,
createdAt: new Date().toISOString(),
},
],
});
const result = buildReducedStepPrompt(task, 1, "/repo/project");
expect(result).not.toContain("ask for context");
expect(result).toContain(".fusion/tasks/FN-456/attachments/");
expect(result).toContain("/repo/project/.fusion/tasks/FN-456/attachments/");
});
it("omits attachment reference when attachments is undefined", () => {
const task = makeTaskDetail({ prompt: reducedPrompt, attachments: undefined });
const result = buildReducedStepPrompt(task, 1);
const result = buildReducedStepPrompt(task, 1, "/repo/project");
expect(result).not.toContain("attachment(s) available");
expect(result).not.toContain(".fusion/tasks/FN-001/attachments/");
});
it("omits attachment reference when attachments is empty", () => {
const task = makeTaskDetail({ prompt: reducedPrompt, attachments: [] });
const result = buildReducedStepPrompt(task, 1);
const result = buildReducedStepPrompt(task, 1, "/repo/project");
expect(result).not.toContain("attachment(s) available");
expect(result).not.toContain(".fusion/tasks/FN-001/attachments/");
});
});

View File

@@ -555,14 +555,16 @@ function escapeRegex(str: string): string {
*
* @param taskDetail - The task to build a prompt for.
* @param stepIndex - The 0-based step index.
* @param rootDir - Optional project root directory used to render absolute attachment paths.
* @returns A reduced prompt string focused on the current step only.
*/
export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number): string {
export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number, rootDir?: string): string {
const { prompt, id, title, attachments } = taskDetail;
// Extract the step-specific section
const stepSection = extractStepSection(prompt, stepIndex);
const hasAttachments = Boolean(attachments && attachments.length > 0);
const attachmentDir = rootDir ? `${rootDir}/.fusion/tasks/${id}/attachments/` : `.fusion/tasks/${id}/attachments/`;
// Build a minimal prompt that focuses on the step without excessive context
const parts: string[] = [
@@ -574,7 +576,7 @@ export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number
stepSection,
"",
hasAttachments
? `${attachments?.length ?? 0} attachment(s) available at .fusion/tasks/${id}/attachments/ — ask for context if needed.`
? `${attachments?.length ?? 0} attachment(s) available at \`${attachmentDir}\` — read the files there for context. They live at the project root and are readable even when working in a worktree.`
: "",
"",
"IMPORTANT: Your previous attempt hit the context window limit.",
@@ -937,7 +939,7 @@ export class StepSessionExecutor {
const stepPrompt = buildStepPrompt(taskDetail, stepIndex, this.options.rootDir, settings, worktreePath);
// Build reduced step prompt for context-limit recovery (simpler, shorter)
const reducedStepPrompt = buildReducedStepPrompt(taskDetail, stepIndex);
const reducedStepPrompt = buildReducedStepPrompt(taskDetail, stepIndex, this.options.rootDir);
// Acquire semaphore if provided
if (semaphore) {