From ee6d7acec8145d5bd62aaf8b3bebbf63b4240a60 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 13 Jun 2026 12:26:01 -0700 Subject: [PATCH] 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 --- ...375-workflow-attachment-recovery-prompt.md | 5 ++ .../__tests__/step-session-executor.test.ts | 70 ++++++++++++++++--- packages/engine/src/step-session-executor.ts | 8 ++- 3 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 .changeset/fn-6375-workflow-attachment-recovery-prompt.md diff --git a/.changeset/fn-6375-workflow-attachment-recovery-prompt.md b/.changeset/fn-6375-workflow-attachment-recovery-prompt.md new file mode 100644 index 0000000000..6ec4583ee9 --- /dev/null +++ b/.changeset/fn-6375-workflow-attachment-recovery-prompt.md @@ -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. diff --git a/packages/engine/src/__tests__/step-session-executor.test.ts b/packages/engine/src/__tests__/step-session-executor.test.ts index 369fd35932..e16dae7b41 100644 --- a/packages/engine/src/__tests__/step-session-executor.test.ts +++ b/packages/engine/src/__tests__/step-session-executor.test.ts @@ -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/"); }); }); diff --git a/packages/engine/src/step-session-executor.ts b/packages/engine/src/step-session-executor.ts index c6d844aa48..481adec506 100644 --- a/packages/engine/src/step-session-executor.ts +++ b/packages/engine/src/step-session-executor.ts @@ -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) {