diff --git a/src/tools/AgentTool/AgentTool.schema.test.ts b/src/tools/AgentTool/AgentTool.schema.test.ts index 4e4e61abd..2eb905117 100644 --- a/src/tools/AgentTool/AgentTool.schema.test.ts +++ b/src/tools/AgentTool/AgentTool.schema.test.ts @@ -5,6 +5,7 @@ import { join } from 'path' import { AgentTool, assertAgentToolCwdAllowed, + buildAsyncLaunchedToolData, buildWorktreeIsolationFallbackNotice, formatWorktreeIsolationFallbackResultText, fullInputSchema, @@ -205,6 +206,36 @@ describe('AgentTool input schema isolation contract', () => { ) }) + test('buildAsyncLaunchedToolData carries worktree isolation fallback for backgrounded sync agents', () => { + const data = buildAsyncLaunchedToolData({ + agentId: 'agent-bg-1', + description: baseInput.description, + prompt: baseInput.prompt, + canReadOutputFile: true, + worktreeIsolationFallback: true, + }) + + expect(data.worktreeIsolationFallback).toBe(true) + expect(outputSchema().safeParse(data).success).toBe(true) + + const block = AgentTool.mapToolResultToToolResultBlockParam(data, 'toolu_bg') + const text = block.content[0]?.type === 'text' ? block.content[0].text : '' + expect(text).toContain('worktreeIsolationFallback: true') + expect(text).toContain('ran without an isolated worktree') + }) + + test('buildAsyncLaunchedToolData omits fallback when isolation succeeded', () => { + const data = buildAsyncLaunchedToolData({ + agentId: 'agent-bg-2', + description: baseInput.description, + prompt: baseInput.prompt, + canReadOutputFile: false, + worktreeIsolationFallback: false, + }) + + expect(data.worktreeIsolationFallback).toBeUndefined() + }) + test('prefers worktree cwd over explicit cwd when both are present defensively', () => { const worktreePath = join(tmpdir(), 'openclaude-worktree') expect( diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx index ef538da7e..9e5f96f9f 100644 --- a/src/tools/AgentTool/AgentTool.tsx +++ b/src/tools/AgentTool/AgentTool.tsx @@ -216,6 +216,26 @@ export function formatWorktreeIsolationFallbackResultText(): string { return 'worktreeIsolationFallback: true\nnote: Worktree isolation was unavailable; this agent ran without an isolated worktree (edits are not sandboxed in a worktree).'; } +/** Shared async_launched payload for direct-async and sync-to-background paths. */ +export function buildAsyncLaunchedToolData(args: { + agentId: string; + description: string; + prompt: string; + canReadOutputFile: boolean; + worktreeIsolationFallback?: boolean; +}) { + return { + isAsync: true as const, + status: 'async_launched' as const, + agentId: args.agentId, + description: args.description, + prompt: args.prompt, + outputFile: getTaskOutputPath(args.agentId), + canReadOutputFile: args.canReadOutputFile, + ...(args.worktreeIsolationFallback && { worktreeIsolationFallback: true as const }), + }; +} + // Output schema - multi-agent spawned schema added dynamically at runtime when enabled export const outputSchema = lazySchema(() => { const syncOutputSchema = agentToolResultSchema().extend({ @@ -910,16 +930,13 @@ export const AgentTool = buildTool({ }))); const canReadOutputFile = toolUseContext.options.tools.some(t => toolMatchesName(t, FILE_READ_TOOL_NAME) || toolMatchesName(t, BASH_TOOL_NAME)); return { - data: { - isAsync: true as const, - status: 'async_launched' as const, + data: buildAsyncLaunchedToolData({ agentId: agentBackgroundTask.agentId, - description: description, - prompt: prompt, - outputFile: getTaskOutputPath(agentBackgroundTask.agentId), + description, + prompt, canReadOutputFile, - ...(worktreeIsolationFallback && { worktreeIsolationFallback: true as const }), - } + worktreeIsolationFallback, + }), }; } else { // Create an explicit agentId for sync agents @@ -1202,15 +1219,13 @@ export const AgentTool = buildTool({ // Return async_launched result immediately const canReadOutputFile = toolUseContext.options.tools.some(t => toolMatchesName(t, FILE_READ_TOOL_NAME) || toolMatchesName(t, BASH_TOOL_NAME)); return { - data: { - isAsync: true as const, - status: 'async_launched' as const, + data: buildAsyncLaunchedToolData({ agentId: backgroundedTaskId, - description: description, - prompt: prompt, - outputFile: getTaskOutputPath(backgroundedTaskId), - canReadOutputFile - } + description, + prompt, + canReadOutputFile, + worktreeIsolationFallback, + }), }; } }